Compare commits
15 Commits
perf/raw-d
...
feat-cherr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb067feaf | ||
|
|
0b5f57436f | ||
|
|
0c0ed5610e | ||
|
|
dce032a275 | ||
|
|
2599b1d326 | ||
|
|
419e376f46 | ||
|
|
c79fcbeceb | ||
|
|
092cb4c3a5 | ||
|
|
d3f25361f7 | ||
|
|
09c13e86b2 | ||
|
|
04bb219c12 | ||
|
|
ca79cf40b1 | ||
|
|
454c82975e | ||
|
|
c38488dfc4 | ||
|
|
2d0ebedbbb |
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
**/*/node_modules
|
||||
@@ -13,7 +13,7 @@ SESSION_SECRET='add some secret here'
|
||||
# Hoppscotch App Domain Config
|
||||
REDIRECT_URL="http://localhost:3000"
|
||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
|
||||
# Google Auth Config
|
||||
GOOGLE_CLIENT_ID="************************************************"
|
||||
|
||||
66
.github/workflows/release-push-docker.yml
vendored
@@ -1,66 +0,0 @@
|
||||
name: "Push containers to Docker Hub on release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./prod.Dockerfile
|
||||
target: backend
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./prod.Dockerfile
|
||||
target: app
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./prod.Dockerfile
|
||||
target: sh_admin
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./prod.Dockerfile
|
||||
target: aio
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
|
||||
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging, "release/**"]
|
||||
branches: [main, staging]
|
||||
pull_request:
|
||||
branches: [main, staging, "release/**"]
|
||||
branches: [main, staging]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
trailingComma: "es5",
|
||||
singleQuote: false,
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
@@ -22,17 +22,17 @@ community include:
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
@@ -106,27 +106,23 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
190
README.md
@@ -2,18 +2,23 @@
|
||||
<a href="https://hoppscotch.io">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/56705483"
|
||||
alt="Hoppscotch"
|
||||
alt="Hoppscotch Logo"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<h3>
|
||||
<br />
|
||||
<p>
|
||||
<h3>
|
||||
<b>
|
||||
Hoppscotch
|
||||
</b>
|
||||
</h3>
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
Hoppscotch
|
||||
Open source API development ecosystem
|
||||
</b>
|
||||
</h3>
|
||||
<b>
|
||||
Open Source API Development Ecosystem
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
|
||||
@@ -29,18 +34,23 @@
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://hoppscotch.io">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
|
||||
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||
</picture>
|
||||
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
|
||||
<img
|
||||
src="./packages/hoppscotch-common/public/images/banner-light.png"
|
||||
alt="Hoppscotch"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
|
||||
<img
|
||||
src="./packages/hoppscotch-common/public/images/banner-dark.png"
|
||||
alt="Hoppscotch"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
|
||||
|
||||
#### **Support**
|
||||
|
||||
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
||||
@@ -49,9 +59,9 @@ _We highly recommend you take a look at the [**Hoppscotch Documentation**](https
|
||||
|
||||
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
||||
|
||||
⚡️ **Fast:** Send requests and get responses in real time.
|
||||
⚡️ **Fast:** Send requests and get/copy responses in real-time.
|
||||
|
||||
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
|
||||
**HTTP Methods**
|
||||
|
||||
- `GET` - Requests retrieve resource information
|
||||
- `POST` - The server creates a new entry in a database
|
||||
@@ -64,15 +74,17 @@ _We highly recommend you take a look at the [**Hoppscotch Documentation**](https
|
||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
||||
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||
|
||||
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||
|
||||
- Choose a theme: System preference, Light, Dark, and Black
|
||||
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
||||
**Theming**
|
||||
|
||||
- Choose a theme: System (default), Light, Dark, and Black
|
||||
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
||||
- Distraction-free Zen mode
|
||||
|
||||
_Customized themes are synced with your cloud/local session._
|
||||
_Customized themes are synced with cloud / local session_
|
||||
|
||||
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
|
||||
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
|
||||
|
||||
- Instant loading with Service Workers
|
||||
- Offline support
|
||||
@@ -95,7 +107,7 @@ _Customized themes are synced with your cloud/local session._
|
||||
|
||||
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
||||
|
||||
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
|
||||
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
|
||||
|
||||
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
||||
|
||||
@@ -115,7 +127,7 @@ _Customized themes are synced with your cloud/local session._
|
||||
- OAuth 2.0
|
||||
- OIDC Access Token/PKCE
|
||||
|
||||
📢 **Headers:** Describes the format the body of your request is being sent in.
|
||||
📢 **Headers:** Describes the format the body of your request is being sent as.
|
||||
|
||||
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
||||
|
||||
@@ -125,14 +137,14 @@ _Customized themes are synced with your cloud/local session._
|
||||
- FormData, JSON, and many more
|
||||
- Toggle between key-value and RAW input parameter list
|
||||
|
||||
📮 **Response:** Contains the status line, headers, and the message/response body.
|
||||
👋 **Response:** Contains the status line, headers, and the message/response body.
|
||||
|
||||
- Copy the response to the clipboard
|
||||
- Download the response as a file
|
||||
- Copy response to clipboard
|
||||
- Download response as a file
|
||||
- View response headers
|
||||
- View raw and preview HTML, image, JSON, and XML responses
|
||||
- View raw and preview of HTML, image, JSON, XML responses
|
||||
|
||||
⏰ **History:** Request entries are synced with your cloud/local session storage.
|
||||
⏰ **History:** Request entries are synced with cloud / local session storage to restore with a single click.
|
||||
|
||||
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
||||
|
||||
@@ -140,32 +152,7 @@ _Customized themes are synced with your cloud/local session._
|
||||
- Nested folders
|
||||
- Export and import as a file or GitHub gist
|
||||
|
||||
_Collections are synced with your cloud/local session storage._
|
||||
|
||||
📜 **Pre-Request Scripts:** Snippets of code associated with a request that is executed before the request is sent.
|
||||
|
||||
- Set environment variables
|
||||
- Include timestamp in the request headers
|
||||
- Send a random alphanumeric string in the URL parameters
|
||||
- Any JavaScript functions
|
||||
|
||||
👨👩👧👦 **Teams:** Helps you collaborate across your teams to design, develop, and test APIs faster.
|
||||
|
||||
- Create unlimited teams
|
||||
- Create unlimited shared collections
|
||||
- Create unlimited team members
|
||||
- Role-based access control
|
||||
- Cloud sync
|
||||
- Multiple devices
|
||||
|
||||
👥 **Workspaces:** Organize your personal and team collections environments into workspaces. Easily switch between workspaces to manage multiple projects.
|
||||
|
||||
- Create unlimited workspaces
|
||||
- Switch between personal and team workspaces
|
||||
|
||||
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
||||
|
||||
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
||||
_Collections are synced with cloud / local session storage_
|
||||
|
||||
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
||||
|
||||
@@ -174,31 +161,60 @@ _Collections are synced with your cloud/local session storage._
|
||||
- Access APIs served in non-HTTPS (`http://`) endpoints
|
||||
- Use your Proxy URL
|
||||
|
||||
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**._
|
||||
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**_
|
||||
|
||||
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
|
||||
|
||||
- Set environment variables
|
||||
- Include timestamp in the request headers
|
||||
- Send a random alphanumeric string in the URL parameters
|
||||
- Any JavaScript functions
|
||||
|
||||
📄 **API Documentation:** Create and share dynamic API documentation easily, quickly.
|
||||
|
||||
1. Add your requests to Collections and Folders
|
||||
2. Export Collections and easily share your APIs with the rest of your team
|
||||
3. Import Collections and Generate Documentation on-the-go
|
||||
|
||||
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
||||
|
||||
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
||||
|
||||
🌎 **i18n:** Experience the app in your language.
|
||||
|
||||
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md) and the process for submitting pull requests to us.
|
||||
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
|
||||
|
||||
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
|
||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
||||
|
||||
**Sign in with:**
|
||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
|
||||
- **[CLI β](https://github.com/hoppscotch/hopp-cli)** - A CLI solution for Hoppscotch
|
||||
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that simplifies access to Hoppscotch
|
||||
|
||||
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
||||
|
||||
> **Extensions fixes `CORS` issues.**
|
||||
|
||||
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
|
||||
|
||||
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
||||
|
||||
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
|
||||
|
||||
**Sign in with**
|
||||
|
||||
- GitHub
|
||||
- Google
|
||||
- Microsoft
|
||||
- Email
|
||||
- SSO (Single Sign-On)[^EE]
|
||||
|
||||
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
|
||||
**Synchronize your data**
|
||||
|
||||
- Workspaces
|
||||
- History
|
||||
- Collections
|
||||
- Environments
|
||||
- Settings
|
||||
|
||||
✅ **Post-Request Tests:** Write tests associated with a request that is executed after the request's response.
|
||||
✅ **Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
|
||||
|
||||
- Check the status code as an integer
|
||||
- Filter response headers
|
||||
@@ -206,7 +222,7 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
|
||||
- Set environment variables
|
||||
- Write JavaScript code
|
||||
|
||||
🌱 **Environments:** Environment variables allow you to store and reuse values in your requests and scripts.
|
||||
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
|
||||
|
||||
- Unlimited environments and variables
|
||||
- Initialize through the pre-request script
|
||||
@@ -225,31 +241,22 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
|
||||
|
||||
</details>
|
||||
|
||||
👨👩👧👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
|
||||
|
||||
- Unlimited teams
|
||||
- Unlimited shared collections
|
||||
- Unlimited team members
|
||||
- Role-based access control
|
||||
- Cloud sync
|
||||
- Multiple devices
|
||||
|
||||
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||
|
||||
- Entries are separated by newline
|
||||
- Keys and values are separated by `:`
|
||||
- Prepend `#` to any row you want to add but keep disabled
|
||||
|
||||
🎛️ **Admin dashboard:** Manage your team and invite members.
|
||||
|
||||
- Insights
|
||||
- Manage users
|
||||
- Manage teams
|
||||
|
||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
||||
|
||||
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
|
||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
|
||||
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
|
||||
|
||||
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
||||
|
||||
> **Extensions fix `CORS` issues.**
|
||||
|
||||
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
||||
|
||||
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
|
||||
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
|
||||
|
||||
## **Demo**
|
||||
|
||||
@@ -261,9 +268,18 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
2. Click "Send" to simulate the request
|
||||
3. View the response
|
||||
|
||||
## **Built with**
|
||||
|
||||
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
|
||||
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Windi CSS](https://windicss.org)
|
||||
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Vue](https://vuejs.org)
|
||||
- [Vite](https://vitejs.dev)
|
||||
|
||||
## **Developing**
|
||||
|
||||
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
||||
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
||||
|
||||
## **Contributing**
|
||||
|
||||
@@ -281,7 +297,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
|
||||
|
||||
## **Authors**
|
||||
|
||||
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
|
||||
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
||||
@@ -293,6 +309,4 @@ This project owes its existence to the collective efforts of all those who contr
|
||||
|
||||
## **License**
|
||||
|
||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) — see the [`LICENSE`](LICENSE) file for details.
|
||||
|
||||
[^EE]: Enterprise edition feature. [Learn more](https://docs.hoppscotch.io/documentation/self-host/getting-started).
|
||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
This document outlines security procedures and general policies for the Hoppscotch project.
|
||||
|
||||
- [Security Policy](#security-policy)
|
||||
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
||||
- [Incident response process](#incident-response-process)
|
||||
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
||||
3. [Incident response process](#incident-response-process)
|
||||
|
||||
## Reporting a security vulnerability
|
||||
|
||||
|
||||
@@ -9,24 +9,26 @@ Before you start working on a new language, please look through the [open pull r
|
||||
if there is no existing translation, you can create a new one by following these steps:
|
||||
|
||||
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||
2. **Checkout the `main` branch for latest translations.**
|
||||
3. **Create a new branch for your translation with base branch `main`.**
|
||||
2. **Checkout the `i18n` branch for latest translations.**
|
||||
3. **Create a new branch for your translation with base branch `i18n`.**
|
||||
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
||||
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
||||
6. **Translate the strings in the target language file.**
|
||||
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
|
||||
8. **Save and commit changes.**
|
||||
8. **Save & commit changes.**
|
||||
9. **Send a pull request.**
|
||||
|
||||
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
|
||||
|
||||
`i18n` branch will be merged into `main` branch once every week.
|
||||
|
||||
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
|
||||
|
||||
## Updating a translation
|
||||
|
||||
### Corrections
|
||||
|
||||
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, [open an issue](https://github.com/hoppscotch/hoppscotch/issues/new/choose).
|
||||
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, open an issue.
|
||||
|
||||
### Broken links
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
:3000 {
|
||||
try_files {path} /
|
||||
root * /site/selfhost-web
|
||||
file_server
|
||||
}
|
||||
|
||||
:3100 {
|
||||
try_files {path} /
|
||||
root * /site/sh-admin
|
||||
file_server
|
||||
}
|
||||
72
aio_run.mjs
@@ -1,72 +0,0 @@
|
||||
#!/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)
|
||||
})
|
||||
@@ -8,25 +8,23 @@ services:
|
||||
hoppscotch-backend:
|
||||
container_name: hoppscotch-backend
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||
context: .
|
||||
target: backend
|
||||
target: prod
|
||||
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
|
||||
- PORT=3000
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
- ./packages/hoppscotch-backend/:/usr/src/app/packages/hoppscotch-backend
|
||||
- ./packages/hoppscotch-backend/:/usr/src/app
|
||||
- /usr/src/app/node_modules/
|
||||
depends_on:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
- hoppscotch-db
|
||||
ports:
|
||||
- "3170:3170"
|
||||
- "3170:3000"
|
||||
|
||||
# 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
|
||||
@@ -34,9 +32,8 @@ services:
|
||||
hoppscotch-app:
|
||||
container_name: hoppscotch-app
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
||||
context: .
|
||||
target: app
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
@@ -50,9 +47,8 @@ services:
|
||||
hoppscotch-sh-admin:
|
||||
container_name: hoppscotch-sh-admin
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
||||
context: .
|
||||
target: sh_admin
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
@@ -60,91 +56,16 @@ services:
|
||||
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
|
||||
image: postgres
|
||||
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:
|
||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||
context: .
|
||||
target: prod
|
||||
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=3000
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||
- /usr/src/app/node_modules/
|
||||
depends_on:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3170:3000"
|
||||
|
||||
hoppscotch-old-app:
|
||||
container_name: hoppscotch-old-app
|
||||
build:
|
||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
||||
context: .
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- hoppscotch-old-backend
|
||||
ports:
|
||||
- "3000:8080"
|
||||
|
||||
hoppscotch-old-sh-admin:
|
||||
container_name: hoppscotch-old-sh-admin
|
||||
build:
|
||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
||||
context: .
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- hoppscotch-old-backend
|
||||
ports:
|
||||
- "3100:8080"
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/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"
|
||||
@@ -32,14 +32,5 @@
|
||||
"@types/node": "^17.0.24",
|
||||
"cross-env": "^7.0.3",
|
||||
"http-server": "^14.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
"httpsnippet@^3.0.1": {
|
||||
"peerDependencies": {
|
||||
"ajv": "6.12.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@lezer/lr": "^1.3.10"
|
||||
"@codemirror/language": "^6.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.0",
|
||||
"@lezer/generator": "^1.1.0",
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-dts": "^4.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.8.0",
|
||||
"version": "2023.4.7",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -21,8 +21,7 @@
|
||||
"test:cov": "jest --coverage",
|
||||
"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",
|
||||
"do-test": "pnpm run test",
|
||||
"seed": "node --loader ts-node/esm prisma/seed.ts"
|
||||
"do-test": "pnpm run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.8.1",
|
||||
@@ -34,7 +33,7 @@
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
"@nestjs/throttler": "^4.0.0",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"@prisma/client": "^4.7.1",
|
||||
"apollo-server-express": "^3.11.1",
|
||||
"apollo-server-plugin-base": "^3.7.1",
|
||||
"argon2": "^0.30.3",
|
||||
@@ -58,8 +57,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-microsoft": "^1.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"prisma": "^4.16.2",
|
||||
"prisma": "^4.7.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.6.0"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TeamMember_userUid_idx" ON "TeamMember"("userUid");
|
||||
@@ -5,7 +5,7 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
|
||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
||||
}
|
||||
|
||||
model Team {
|
||||
@@ -26,7 +26,6 @@ model TeamMember {
|
||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([teamID, userUid])
|
||||
@@index([userUid])
|
||||
}
|
||||
|
||||
model TeamInvitation {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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);
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('ping')
|
||||
export class AppController {
|
||||
@Get()
|
||||
ping(): string {
|
||||
return 'Success';
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,6 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
|
||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||
import { COOKIES_NOT_FOUND } from './errors';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AppController } from './app.controller';
|
||||
import { DbModule } from './db/db.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -67,7 +65,6 @@ import { DbModule } from './db/db.module';
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
}),
|
||||
DbModule,
|
||||
UserModule,
|
||||
AuthModule,
|
||||
AdminModule,
|
||||
@@ -84,6 +81,5 @@ import { DbModule } from './db/db.module';
|
||||
ShortcodeModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
|
||||
* Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable
|
||||
*
|
||||
* @param provider Provider we want to check the presence of
|
||||
* @returns Boolean if provider specified is present or not
|
||||
@@ -117,8 +117,8 @@ export function authProviderCheck(provider: string) {
|
||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
const envVariables = process.env.ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
provider.trim().toUpperCase(),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||
@@ -1,21 +0,0 @@
|
||||
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 {}
|
||||
@@ -29,22 +29,22 @@ export const JSON_INVALID = 'json_invalid';
|
||||
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||
|
||||
/**
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||
*/
|
||||
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||
'"ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||
|
||||
/**
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||
*/
|
||||
export const ENV_EMPTY_AUTH_PROVIDERS =
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||
'"ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||
|
||||
/**
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||
*/
|
||||
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||
'"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||
|
||||
/**
|
||||
* Tried to delete a user data document from fb firestore but failed.
|
||||
|
||||
@@ -7,11 +7,7 @@ export class PrismaService
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
log: ['query', 'info', 'warn', 'error'],
|
||||
}
|
||||
);
|
||||
super();
|
||||
}
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
|
||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
orderBy: {
|
||||
createdOn: 'desc',
|
||||
},
|
||||
skip: args.cursor ? 1 : 0,
|
||||
skip: 1,
|
||||
take: args.take,
|
||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||
});
|
||||
|
||||
@@ -306,8 +306,8 @@ describe('TeamEnvironmentsService', () => {
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
@@ -337,8 +337,8 @@ describe('TeamEnvironmentsService', () => {
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
|
||||
@@ -22,7 +22,6 @@ import { throwErr } from 'src/utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { cons } from 'fp-ts/lib/ReadonlyNonEmptyArray';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Team)
|
||||
@@ -56,13 +55,8 @@ export class TeamResolver {
|
||||
description: 'Returns the list of members of a team',
|
||||
complexity: 10,
|
||||
})
|
||||
async teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
|
||||
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;
|
||||
teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
|
||||
return this.teamService.getTeamMembers(team.id);
|
||||
}
|
||||
|
||||
@ResolveField(() => TeamMemberRole, {
|
||||
@@ -70,61 +64,41 @@ export class TeamResolver {
|
||||
nullable: true,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async myRole(
|
||||
myRole(
|
||||
@Parent() team: Team,
|
||||
@GqlUser() user: AuthUser,
|
||||
): Promise<TeamMemberRole | null> {
|
||||
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;
|
||||
return this.teamService.getRoleOfUserInTeam(team.id, user.uid);
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the OWNER role in the team',
|
||||
})
|
||||
async ownersCount(@Parent() team: Team): Promise<number> {
|
||||
const startR = Date.now();
|
||||
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
ownersCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.OWNER,
|
||||
);
|
||||
const endR = Date.now();
|
||||
console.log('response generation: (ownersCount)', endR - startR, 'ms');
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the EDITOR role in the team',
|
||||
})
|
||||
async editorsCount(@Parent() team: Team): Promise<number> {
|
||||
const startR = Date.now();
|
||||
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
editorsCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.EDITOR,
|
||||
);
|
||||
const endR = Date.now();
|
||||
console.log('response generation: (editorsCount)', endR - startR, 'ms');
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the VIEWER role in the team',
|
||||
})
|
||||
async viewersCount(@Parent() team: Team): Promise<number> {
|
||||
const startR = Date.now();
|
||||
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
viewersCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.VIEWER,
|
||||
);
|
||||
const endR = Date.now();
|
||||
console.log('response generation: (viewersCount)', endR - startR, 'ms');
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Query
|
||||
@@ -132,7 +106,7 @@ export class TeamResolver {
|
||||
description: 'List of teams that the executing user belongs to.',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async myTeams(
|
||||
myTeams(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'cursor',
|
||||
@@ -143,15 +117,7 @@ export class TeamResolver {
|
||||
})
|
||||
cursor?: string,
|
||||
): Promise<Team[]> {
|
||||
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;
|
||||
return this.teamService.getTeamsOfUser(user.uid, cursor ?? null);
|
||||
}
|
||||
|
||||
@Query(() => Team, {
|
||||
@@ -164,7 +130,7 @@ export class TeamResolver {
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.OWNER,
|
||||
)
|
||||
async team(
|
||||
team(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
@@ -172,12 +138,7 @@ export class TeamResolver {
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<Team | null> {
|
||||
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;
|
||||
return this.teamService.getTeamWithID(teamID);
|
||||
}
|
||||
|
||||
// Mutation
|
||||
@@ -190,11 +151,7 @@ export class TeamResolver {
|
||||
@Args({ name: 'name', description: 'Displayed name of the team' })
|
||||
name: string,
|
||||
): Promise<Team> {
|
||||
const startR = Date.now();
|
||||
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);
|
||||
return team.right;
|
||||
}
|
||||
@@ -212,11 +169,7 @@ export class TeamResolver {
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const startR = Date.now();
|
||||
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);
|
||||
return isUserLeft.right;
|
||||
}
|
||||
@@ -241,11 +194,7 @@ export class TeamResolver {
|
||||
})
|
||||
userUid: string,
|
||||
): Promise<boolean> {
|
||||
const startR = Date.now();
|
||||
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);
|
||||
return isRemoved.right;
|
||||
}
|
||||
@@ -261,11 +210,7 @@ export class TeamResolver {
|
||||
@Args({ name: 'newName', description: 'The updated name of the team' })
|
||||
newName: string,
|
||||
): Promise<Team> {
|
||||
const startR = Date.now();
|
||||
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);
|
||||
return team.right;
|
||||
}
|
||||
@@ -279,11 +224,7 @@ export class TeamResolver {
|
||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const startR = Date.now();
|
||||
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);
|
||||
return isDeleted.right;
|
||||
}
|
||||
@@ -313,19 +254,11 @@ export class TeamResolver {
|
||||
})
|
||||
newRole: TeamMemberRole,
|
||||
): Promise<TeamMember> {
|
||||
const startR = Date.now();
|
||||
const teamMember = await this.teamService.updateTeamMemberRole(
|
||||
teamID,
|
||||
userUid,
|
||||
newRole,
|
||||
);
|
||||
const endR = Date.now();
|
||||
console.log(
|
||||
'response generation: (updateTeamMemberRole)',
|
||||
endR - startR,
|
||||
'ms',
|
||||
);
|
||||
|
||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||
return teamMember.right;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { TeamMember, TeamMemberRole, Team } from './team.model';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TeamMember as DbTeamMember } from '@prisma/client';
|
||||
@@ -23,8 +23,6 @@ import * as T from 'fp-ts/Task';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { AuthUser } from '../types/AuthUser';
|
||||
import { PG_CONNECTION } from 'src/constants';
|
||||
import { Client } from 'pg';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
@@ -32,11 +30,8 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly userService: UserService,
|
||||
private readonly pubsub: PubSubService,
|
||||
@Inject(PG_CONNECTION) private conn: Client,
|
||||
) {}
|
||||
|
||||
enableRawSql: boolean = false;
|
||||
|
||||
onModuleInit() {
|
||||
this.userService.registerUserDataHandler(this);
|
||||
}
|
||||
@@ -57,37 +52,12 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
teamID: string,
|
||||
role: TeamMemberRole,
|
||||
): Promise<number> {
|
||||
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({
|
||||
return await this.prisma.teamMember.count({
|
||||
where: {
|
||||
teamID,
|
||||
role,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getCountOfUsersWithRoleInTeam >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms >>>>>',
|
||||
count,
|
||||
);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
async addMemberToTeamWithEmail(
|
||||
@@ -107,11 +77,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
uid: string,
|
||||
role: TeamMemberRole,
|
||||
): 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({
|
||||
data: {
|
||||
userUid: uid,
|
||||
@@ -136,31 +101,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
}
|
||||
|
||||
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({
|
||||
where: {
|
||||
id: teamID,
|
||||
@@ -179,8 +119,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
id: teamID,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log('deleteTeam >>>>>>>>>>', endQ - startQ, 'ms', '>>>>>', team);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
@@ -197,26 +135,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
const isValidTitle = this.validateTeamName(newName);
|
||||
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 {
|
||||
const updatedTeam = await this.prisma.team.update({
|
||||
where: {
|
||||
@@ -238,48 +156,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
userUid: string,
|
||||
newRole: TeamMemberRole,
|
||||
): 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({
|
||||
where: {
|
||||
teamID,
|
||||
@@ -316,14 +192,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
role: newRole,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'updateTeamMemberRole >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
result,
|
||||
);
|
||||
|
||||
const updatedMember: TeamMember = {
|
||||
membershipID: result.id,
|
||||
@@ -340,30 +208,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
teamID: string,
|
||||
userUid: string,
|
||||
): 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({
|
||||
where: {
|
||||
teamID,
|
||||
@@ -404,34 +248,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
const isValidName = this.validateTeamName(name);
|
||||
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({
|
||||
data: {
|
||||
name: name,
|
||||
@@ -443,42 +259,12 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
},
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log('createTeam >>>>>>>>>> ', endQ - startQ, 'ms', '>>>>>', team);
|
||||
|
||||
return E.right(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) {
|
||||
const startQ = Date.now();
|
||||
const entries = await this.prisma.teamMember.findMany({
|
||||
take: 10,
|
||||
where: {
|
||||
@@ -488,18 +274,9 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getTeamsOfUser >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
entries,
|
||||
);
|
||||
|
||||
return entries.map((entry) => entry.team);
|
||||
} else {
|
||||
const startQ = Date.now();
|
||||
const entries = await this.prisma.teamMember.findMany({
|
||||
take: 10,
|
||||
skip: 1,
|
||||
@@ -516,56 +293,17 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getTeamsOfUser >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
entries,
|
||||
);
|
||||
|
||||
return entries.map((entry) => entry.team);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const startQ = Date.now();
|
||||
const team = await this.prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamID,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getTeamWithID >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
team,
|
||||
);
|
||||
|
||||
return team;
|
||||
} catch (_e) {
|
||||
@@ -615,30 +353,7 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
teamID: string,
|
||||
userUid: string,
|
||||
): 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 {
|
||||
const startQ = Date.now();
|
||||
const teamMember = await this.prisma.teamMember.findUnique({
|
||||
where: {
|
||||
teamID_userUid: {
|
||||
@@ -647,14 +362,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
},
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getTeamMember >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
teamMember,
|
||||
);
|
||||
|
||||
if (!teamMember) return null;
|
||||
|
||||
@@ -726,44 +433,11 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
}
|
||||
|
||||
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({
|
||||
where: {
|
||||
teamID,
|
||||
},
|
||||
});
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getTeamMembers >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
dbTeamMembers,
|
||||
);
|
||||
|
||||
const members = dbTeamMembers.map(
|
||||
(entry) =>
|
||||
@@ -796,39 +470,8 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
teamID: string,
|
||||
cursor: string | null,
|
||||
): 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[];
|
||||
|
||||
const startQ = Date.now();
|
||||
if (!cursor) {
|
||||
teamMembers = await this.prisma.teamMember.findMany({
|
||||
take: 10,
|
||||
@@ -848,14 +491,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
||||
},
|
||||
});
|
||||
}
|
||||
const endQ = Date.now();
|
||||
console.log(
|
||||
'getMembersOfTeam >>>>>>>>>>',
|
||||
endQ - startQ,
|
||||
'ms',
|
||||
'>>>>>',
|
||||
teamMembers,
|
||||
);
|
||||
|
||||
const members = teamMembers.map(
|
||||
(entry) =>
|
||||
|
||||
@@ -24,8 +24,6 @@ beforeEach(() => {
|
||||
mockPubSub.publish.mockClear();
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
|
||||
describe('UserHistoryService', () => {
|
||||
describe('fetchUserHistory', () => {
|
||||
test('Should return a list of users REST history if exists', async () => {
|
||||
@@ -402,7 +400,7 @@ describe('UserHistoryService', () => {
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: date,
|
||||
executedOn: new Date(),
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -412,7 +410,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: date,
|
||||
executedOn: new Date(),
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,7 @@ import * as E from 'fp-ts/Either';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { TeamMemberRole } from './team/team.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';
|
||||
|
||||
/**
|
||||
@@ -161,21 +156,21 @@ export function isValidLength(title: string, length: number) {
|
||||
|
||||
/**
|
||||
* This function is called by bootstrap() in main.ts
|
||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* If not, it throws an error.
|
||||
*/
|
||||
export function checkEnvironmentAuthProvider() {
|
||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||
if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) {
|
||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
if (process.env.ALLOWED_AUTH_PROVIDERS === '') {
|
||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||
',',
|
||||
).map((provider) => provider.toLocaleUpperCase());
|
||||
const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||
(provider) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||
(provider: string) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
|
||||
128
packages/hoppscotch-cli/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
support@hoppscotch.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
21
packages/hoppscotch-cli/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,19 +1,29 @@
|
||||
<div align="center">
|
||||
<a href="https://hoppscotch.io">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/56705483"
|
||||
alt="Hoppscotch Logo"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
|
||||
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
||||
|
||||
A CLI to run Hoppscotch Test Scripts in CI environments.
|
||||
</div>
|
||||
|
||||
A CLI to run Hoppscotch test scripts in CI environments.
|
||||
|
||||
### **Commands:**
|
||||
|
||||
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
||||
|
||||
### **Usage:**
|
||||
|
||||
```bash
|
||||
```
|
||||
hopp [options or commands] arguments
|
||||
```
|
||||
|
||||
### **Options:**
|
||||
|
||||
- `-v`, `--ver`: see the current version of the CLI
|
||||
- `-h`, `--help`: display help for command
|
||||
|
||||
@@ -35,21 +45,17 @@ hopp [options or commands] arguments
|
||||
- Executes and outputs test-script response.
|
||||
|
||||
#### Options:
|
||||
|
||||
##### `-e <file_path>` / `--env <file_path>`
|
||||
|
||||
- Accepts path to env.json with contents in below format:
|
||||
|
||||
```json
|
||||
{
|
||||
"ENV1":"value1",
|
||||
"ENV2":"value2"
|
||||
}
|
||||
```
|
||||
|
||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
|
||||
## Install
|
||||
|
||||
@@ -69,59 +75,4 @@ npm i -g @hoppscotch/cli
|
||||
|
||||
## **Contributing:**
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||
build.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
|
||||
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
|
||||
do not have permission to do that, you may request the second reviewer merge it for you.
|
||||
|
||||
## Set Up The Development Environment
|
||||
|
||||
1. After cloning the repository, execute the following commands:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
2. In order to test locally, you can use two types of package linking:
|
||||
|
||||
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
|
||||
|
||||
```bash
|
||||
pnpm link @hoppscotch/cli
|
||||
|
||||
// Then to use or test the CLI:
|
||||
pnpm exec hopp
|
||||
|
||||
// After testing, to remove the package linking:
|
||||
pnpm rm @hoppscotch/cli
|
||||
```
|
||||
|
||||
2. The 'global' way (warning: this might override the globally installed CLI, if exists):
|
||||
|
||||
```bash
|
||||
sudo pnpm link --global
|
||||
|
||||
// Then to use or test the CLI:
|
||||
hopp
|
||||
|
||||
// After testing, to remove the package linking:
|
||||
sudo pnpm rm --global @hoppscotch/cli
|
||||
```
|
||||
|
||||
3. To use the Typescript watch scripts:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**
|
||||
|
||||
@@ -29,18 +29,8 @@ module.exports = {
|
||||
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||
"no-console": "off",
|
||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"prettier/prettier": [
|
||||
"prettier/prettier":
|
||||
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/no-side-effects-in-computed-properties": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
trailingComma: "es5",
|
||||
singleQuote: false,
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
}
|
||||
|
||||
@@ -166,6 +166,12 @@ a {
|
||||
@apply truncate;
|
||||
@apply sm:inline-flex;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
@@ -184,11 +190,10 @@ a {
|
||||
@apply border-solid border-dividerDark;
|
||||
@apply rounded;
|
||||
@apply shadow-lg;
|
||||
@apply max-w-[45vw] #{!important};
|
||||
|
||||
.tippy-content {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-[45vh];
|
||||
@apply max-h-56;
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@@ -196,10 +201,6 @@ a {
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
& > span {
|
||||
@apply block #{!important};
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
@@ -215,7 +216,6 @@ a {
|
||||
|
||||
[data-v-tippy] {
|
||||
@apply flex flex-1;
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
[interactive] > div {
|
||||
@@ -326,7 +326,7 @@ pre.ace_editor {
|
||||
@apply after:font-icon;
|
||||
@apply after:text-current;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e5cf"];
|
||||
@apply after:content-["\e313"];
|
||||
@apply after:text-lg;
|
||||
}
|
||||
|
||||
@@ -481,10 +481,6 @@ pre.ace_editor {
|
||||
}
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply overscroll-y-auto;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
.cm-line::selection {
|
||||
@apply bg-accentDark #{!important};
|
||||
@@ -572,11 +568,3 @@ details[open] summary .indicator {
|
||||
@apply rounded;
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.gql-operation-not-highlight {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
.gql-operation-highlight {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@mixin base-theme {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-icon: "Material Symbols Rounded Variable";
|
||||
--font-mono: "Roboto Mono Variable", monospace;
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
--font-icon: "Material Icons";
|
||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"rename": "Rename",
|
||||
"remove": "Remove",
|
||||
"restore": "Restore",
|
||||
"save": "Save",
|
||||
@@ -69,8 +68,6 @@
|
||||
"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_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",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"name": "Hoppscotch",
|
||||
@@ -135,7 +132,6 @@
|
||||
"renamed": "Collection renamed",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Save as",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"select": "Select a Collection",
|
||||
"select_location": "Select location",
|
||||
"select_team": "Select a team",
|
||||
@@ -153,14 +149,12 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||
},
|
||||
"context_menu": {
|
||||
"set_environment_variable": "Set as variable",
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab"
|
||||
"add_parameter": "Add to parameter",
|
||||
"open_link_in_new_tab": "Open link in new tab"
|
||||
},
|
||||
"count": {
|
||||
"header": "Header {count}",
|
||||
@@ -185,6 +179,7 @@
|
||||
"folder": "Folder is empty",
|
||||
"headers": "This request does not have any headers",
|
||||
"history": "History is empty",
|
||||
"history_suggestions": "History does not have any matching entries",
|
||||
"invites": "Invite list is empty",
|
||||
"members": "Team is empty",
|
||||
"parameters": "This request does not have any parameters",
|
||||
@@ -204,25 +199,18 @@
|
||||
"create_new": "Create new environment",
|
||||
"created": "Environment created",
|
||||
"deleted": "Environment deletion",
|
||||
"duplicated": "Environment duplicated",
|
||||
"edit": "Edit Environment",
|
||||
"global": "Global",
|
||||
"empty_variables": "No variables",
|
||||
"global_variables": "Global variables",
|
||||
"invalid_name": "Please provide a name for the environment",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
"name": "Name",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "New Environment",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_environment": "No environment",
|
||||
"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",
|
||||
"scope": "Scope",
|
||||
"select": "Select environment",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Environments",
|
||||
@@ -252,7 +240,6 @@
|
||||
"no_duration": "No duration",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
@@ -280,10 +267,6 @@
|
||||
"graphql": {
|
||||
"mutations": "Mutations",
|
||||
"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"
|
||||
},
|
||||
"group": {
|
||||
@@ -313,30 +296,6 @@
|
||||
"preview": "Hide Preview",
|
||||
"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": {
|
||||
"collections": "Import collections",
|
||||
"curl": "Import cURL",
|
||||
@@ -473,10 +432,8 @@
|
||||
"payload": "Payload",
|
||||
"query": "Query",
|
||||
"raw_body": "Raw Request Body",
|
||||
"rename": "Rename Request",
|
||||
"renamed": "Request renamed",
|
||||
"run": "Run",
|
||||
"stop": "Stop",
|
||||
"save": "Save",
|
||||
"save_as": "Save as",
|
||||
"saved": "Request saved",
|
||||
@@ -516,9 +473,9 @@
|
||||
"account_name_description": "This is your display name.",
|
||||
"background": "Background",
|
||||
"black_mode": "Black",
|
||||
"dark_mode": "Dark",
|
||||
"change_font_size": "Change font size",
|
||||
"choose_language": "Choose language",
|
||||
"dark_mode": "Dark",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
@@ -582,10 +539,6 @@
|
||||
"show_all": "Keyboard shortcuts",
|
||||
"title": "General"
|
||||
},
|
||||
"others": {
|
||||
"title": "Others",
|
||||
"prettify": "Prettify Editor's Content"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite people to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
@@ -606,9 +559,6 @@
|
||||
"delete_method": "Select DELETE method",
|
||||
"get_method": "Select GET method",
|
||||
"head_method": "Select HEAD method",
|
||||
"rename": "Rename Request",
|
||||
"import_curl": "Import cURL",
|
||||
"show_code": "Generate code snippet",
|
||||
"method": "Method",
|
||||
"next_method": "Select Next method",
|
||||
"post_method": "Select POST method",
|
||||
@@ -617,7 +567,6 @@
|
||||
"reset_request": "Reset Request",
|
||||
"save_to_collections": "Save to Collections",
|
||||
"send_request": "Send Request",
|
||||
"save_request": "Save Request",
|
||||
"title": "Request"
|
||||
},
|
||||
"response": {
|
||||
@@ -626,10 +575,10 @@
|
||||
"title": "Response"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Switch theme to Black Mode",
|
||||
"dark": "Switch theme to Dark Mode",
|
||||
"light": "Switch theme to Light Mode",
|
||||
"system": "Switch theme to System Mode",
|
||||
"black": "Switch theme to black mode",
|
||||
"dark": "Switch theme to dark mode",
|
||||
"light": "Switch theme to light mode",
|
||||
"system": "Switch theme to system mode",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
@@ -648,87 +597,8 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
"user": "User"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -788,11 +658,8 @@
|
||||
"tab": {
|
||||
"authorization": "Authorization",
|
||||
"body": "Body",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"collections": "Collections",
|
||||
"documentation": "Documentation",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"environments": "Environments",
|
||||
"headers": "Headers",
|
||||
"history": "History",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"edit": "編輯",
|
||||
"filter": "篩選回應",
|
||||
"go_back": "返回",
|
||||
"go_forward": "向前",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "分組方式",
|
||||
"label": "標籤",
|
||||
"learn_more": "瞭解更多",
|
||||
@@ -117,37 +117,37 @@
|
||||
"username": "使用者名稱"
|
||||
},
|
||||
"collection": {
|
||||
"created": "集合已建立",
|
||||
"different_parent": "無法為父集合不同的集合重新排序",
|
||||
"edit": "編輯集合",
|
||||
"invalid_name": "請提供有效的集合名稱",
|
||||
"invalid_root_move": "集合已在根目錄",
|
||||
"moved": "移動成功",
|
||||
"my_collections": "我的集合",
|
||||
"name": "我的新集合",
|
||||
"name_length_insufficient": "集合名稱至少要有 3 個字元。",
|
||||
"new": "建立集合",
|
||||
"order_changed": "集合順序已更新",
|
||||
"renamed": "集合已重新命名",
|
||||
"created": "組合已建立",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "編輯組合",
|
||||
"invalid_name": "請提供有效的組合名稱",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"my_collections": "我的組合",
|
||||
"name": "我的新組合",
|
||||
"name_length_insufficient": "組合名稱至少要有 3 個字元。",
|
||||
"new": "建立組合",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"renamed": "組合已重新命名",
|
||||
"request_in_use": "請求正在使用中",
|
||||
"save_as": "另存為",
|
||||
"select": "選擇一個集合",
|
||||
"select": "選擇一個組合",
|
||||
"select_location": "選擇位置",
|
||||
"select_team": "選擇一個團隊",
|
||||
"team_collections": "團隊集合"
|
||||
"team_collections": "團隊組合"
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "您確定要離開此團隊嗎?",
|
||||
"logout": "您確定要登出嗎?",
|
||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||
"remove_collection": "您確定要永久刪除該組合嗎?",
|
||||
"remove_environment": "您確定要永久刪除該環境嗎?",
|
||||
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
||||
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
||||
"remove_request": "您確定要永久刪除該請求嗎?",
|
||||
"remove_team": "您確定要刪除該團隊嗎?",
|
||||
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
||||
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
|
||||
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
|
||||
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||
},
|
||||
"count": {
|
||||
@@ -160,13 +160,13 @@
|
||||
},
|
||||
"documentation": {
|
||||
"generate": "產生文件",
|
||||
"generate_message": "匯入 Hoppscotch 集合以隨時隨地產生 API 文件。"
|
||||
"generate_message": "匯入 Hoppscotch 組合以隨時隨地產生 API 文件。"
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "該請求沒有使用任何授權",
|
||||
"body": "該請求沒有任何請求主體",
|
||||
"collection": "集合為空",
|
||||
"collections": "集合為空",
|
||||
"collection": "組合為空",
|
||||
"collections": "組合為空",
|
||||
"documentation": "連線到 GraphQL 端點以檢視文件",
|
||||
"endpoint": "端點不能留空",
|
||||
"environments": "環境為空",
|
||||
@@ -209,7 +209,7 @@
|
||||
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
||||
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
||||
"curl_invalid_format": "cURL 格式不正確",
|
||||
"danger_zone": "危險地帶",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
||||
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
||||
"empty_req_name": "空請求名稱",
|
||||
@@ -277,38 +277,38 @@
|
||||
"tests": "編寫測試指令碼以自動除錯。"
|
||||
},
|
||||
"hide": {
|
||||
"collection": "隱藏集合面板",
|
||||
"collection": "隱藏組合面板",
|
||||
"more": "隱藏更多",
|
||||
"preview": "隱藏預覽",
|
||||
"sidebar": "隱藏側邊欄"
|
||||
},
|
||||
"import": {
|
||||
"collections": "匯入集合",
|
||||
"collections": "匯入組合",
|
||||
"curl": "匯入 cURL",
|
||||
"failed": "匯入失敗",
|
||||
"from_gist": "從 Gist 匯入",
|
||||
"from_gist_description": "從 Gist 網址匯入",
|
||||
"from_insomnia": "從 Insomnia 匯入",
|
||||
"from_insomnia_description": "從 Insomnia 集合匯入",
|
||||
"from_insomnia_description": "從 Insomnia 組合匯入",
|
||||
"from_json": "從 Hoppscotch 匯入",
|
||||
"from_json_description": "從 Hoppscotch 集合檔匯入",
|
||||
"from_my_collections": "從我的集合匯入",
|
||||
"from_my_collections_description": "從我的集合檔匯入",
|
||||
"from_json_description": "從 Hoppscotch 組合檔匯入",
|
||||
"from_my_collections": "從我的組合匯入",
|
||||
"from_my_collections_description": "從我的組合檔匯入",
|
||||
"from_openapi": "從 OpenAPI 匯入",
|
||||
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
||||
"from_postman": "從 Postman 匯入",
|
||||
"from_postman_description": "從 Postman 集合匯入",
|
||||
"from_postman_description": "從 Postman 組合匯入",
|
||||
"from_url": "從網址匯入",
|
||||
"gist_url": "輸入 Gist 網址",
|
||||
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
||||
"import_from_url_invalid_file_format": "匯入集合時發生錯誤",
|
||||
"import_from_url_invalid_file_format": "匯入組合時發生錯誤",
|
||||
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||
"import_from_url_success": "已匯入集合",
|
||||
"json_description": "從 Hoppscotch 集合 JSON 檔匯入集合",
|
||||
"import_from_url_success": "已匯入組合",
|
||||
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
|
||||
"title": "匯入"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "隱藏或顯示集合",
|
||||
"collapse_collection": "隱藏或顯示組合",
|
||||
"collapse_sidebar": "隱藏或顯示側邊欄",
|
||||
"column": "垂直版面",
|
||||
"name": "配置",
|
||||
@@ -316,8 +316,8 @@
|
||||
"zen_mode": "專注模式"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "您有未儲存的改動",
|
||||
"collections": "集合",
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "組合",
|
||||
"confirm": "確認",
|
||||
"edit_request": "編輯請求",
|
||||
"import_export": "匯入/匯出"
|
||||
@@ -374,9 +374,9 @@
|
||||
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
||||
"no_permission": "您沒有權限執行此操作。",
|
||||
"owner": "擁有者",
|
||||
"owner_description": "擁有者可以新增、編輯和刪除請求、集合和團隊成員。",
|
||||
"owner_description": "擁有者可以新增、編輯和刪除請求、組合和團隊成員。",
|
||||
"roles": "角色",
|
||||
"roles_description": "角色用來控制對共用集合的存取權。",
|
||||
"roles_description": "角色用來控制對共用組合的存取權。",
|
||||
"updated": "已更新個人檔案",
|
||||
"viewer": "檢視者",
|
||||
"viewer_description": "檢視者只能檢視和使用請求。"
|
||||
@@ -396,8 +396,8 @@
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "複製連結",
|
||||
"different_collection": "無法重新排列來自不同集合的請求",
|
||||
"duplicated": "已複製請求",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "持續時間",
|
||||
"enter_curl": "輸入 cURL",
|
||||
"generate_code": "產生程式碼",
|
||||
@@ -405,10 +405,10 @@
|
||||
"header_list": "請求標頭列表",
|
||||
"invalid_name": "請提供請求名稱",
|
||||
"method": "方法",
|
||||
"moved": "已移動請求",
|
||||
"moved": "Request moved",
|
||||
"name": "請求名稱",
|
||||
"new": "新請求",
|
||||
"order_changed": "已更新請求順序",
|
||||
"order_changed": "Request Order Updated",
|
||||
"override": "覆寫",
|
||||
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
||||
"overriden": "已覆寫",
|
||||
@@ -432,7 +432,7 @@
|
||||
"view_my_links": "檢視我的連結"
|
||||
},
|
||||
"response": {
|
||||
"audio": "音訊",
|
||||
"audio": "Audio",
|
||||
"body": "回應本體",
|
||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||
"headers": "回應標頭",
|
||||
@@ -446,7 +446,7 @@
|
||||
"status": "狀態",
|
||||
"time": "時間",
|
||||
"title": "回應",
|
||||
"video": "視訊",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "等待連線",
|
||||
"xml": "XML"
|
||||
},
|
||||
@@ -494,7 +494,7 @@
|
||||
"short_codes_description": "我們為您打造的快捷碼。",
|
||||
"sidebar_on_left": "左側邊欄",
|
||||
"sync": "同步",
|
||||
"sync_collections": "集合",
|
||||
"sync_collections": "組合",
|
||||
"sync_description": "這些設定會同步到雲端。",
|
||||
"sync_environments": "環境",
|
||||
"sync_history": "歷史",
|
||||
@@ -551,7 +551,7 @@
|
||||
"previous_method": "選擇上一個方法",
|
||||
"put_method": "選擇 PUT 方法",
|
||||
"reset_request": "重置請求",
|
||||
"save_to_collections": "儲存到集合",
|
||||
"save_to_collections": "儲存到組合",
|
||||
"send_request": "傳送請求",
|
||||
"title": "請求"
|
||||
},
|
||||
@@ -570,7 +570,7 @@
|
||||
},
|
||||
"show": {
|
||||
"code": "顯示程式碼",
|
||||
"collection": "顯示集合面板",
|
||||
"collection": "顯示組合面板",
|
||||
"more": "顯示更多",
|
||||
"sidebar": "顯示側邊欄"
|
||||
},
|
||||
@@ -639,9 +639,9 @@
|
||||
"tab": {
|
||||
"authorization": "授權",
|
||||
"body": "請求本體",
|
||||
"collections": "集合",
|
||||
"collections": "組合",
|
||||
"documentation": "幫助文件",
|
||||
"environments": "環境",
|
||||
"environments": "Environments",
|
||||
"headers": "請求標頭",
|
||||
"history": "歷史記錄",
|
||||
"mqtt": "MQTT",
|
||||
@@ -666,7 +666,7 @@
|
||||
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
||||
"exit": "退出團隊",
|
||||
"exit_disabled": "團隊擁有者無法退出團隊",
|
||||
"invalid_coll_id": "集合 ID 無效",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "電子信箱格式無效",
|
||||
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
||||
"invalid_invite_link": "邀請連結無效",
|
||||
@@ -690,21 +690,21 @@
|
||||
"member_removed": "使用者已移除",
|
||||
"member_role_updated": "使用者角色已更新",
|
||||
"members": "成員",
|
||||
"more_members": "還有 {count} 位",
|
||||
"more_members": "+{count} more",
|
||||
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
||||
"name_updated": "團隊名稱已更新",
|
||||
"new": "新團隊",
|
||||
"new_created": "已建立新團隊",
|
||||
"new_name": "我的新團隊",
|
||||
"no_access": "您沒有編輯集合的許可權",
|
||||
"no_access": "您沒有編輯組合的許可權",
|
||||
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
||||
"no_request_found": "找不到請求。",
|
||||
"no_request_found": "Request not found.",
|
||||
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
||||
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
||||
"parent_coll_move": "無法將集合移動至子集合",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"pending_invites": "待定邀請",
|
||||
"permissions": "許可權",
|
||||
"same_target_destination": "目標和目的地相同",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "團隊已儲存",
|
||||
"select_a_team": "選擇團隊",
|
||||
"title": "團隊",
|
||||
@@ -734,9 +734,9 @@
|
||||
"url": "網址"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "切換工作區",
|
||||
"personal": "我的工作區",
|
||||
"team": "團隊工作區",
|
||||
"title": "工作區"
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.8.0",
|
||||
"version": "2023.4.7",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
@@ -22,141 +22,137 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@codemirror/legacy-modes": "^6.3.3",
|
||||
"@codemirror/lint": "^6.4.0",
|
||||
"@codemirror/search": "^6.5.1",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@codemirror/view": "^6.16.0",
|
||||
"@fontsource-variable/inter": "^5.0.8",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||
"@codemirror/autocomplete": "^6.0.3",
|
||||
"@codemirror/commands": "^6.0.1",
|
||||
"@codemirror/lang-javascript": "^6.0.1",
|
||||
"@codemirror/lang-json": "^6.0.0",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/language": "^6.2.0",
|
||||
"@codemirror/legacy-modes": "^6.1.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.1.0",
|
||||
"@codemirror/view": "^6.0.2",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@sentry/tracing": "^7.64.0",
|
||||
"@sentry/vue": "^7.64.0",
|
||||
"@urql/core": "^4.1.1",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@sentry/tracing": "^7.13.0",
|
||||
"@sentry/vue": "^7.13.0",
|
||||
"@urql/core": "^2.5.0",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-auth": "^2.1.6",
|
||||
"@urql/exchange-graphcache": "^6.3.2",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/head": "^1.3.1",
|
||||
"@urql/exchange-auth": "^0.1.7",
|
||||
"@urql/exchange-graphcache": "^4.4.3",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/head": "^0.7.9",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^0.21.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.16.1",
|
||||
"fp-ts": "^2.12.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"globalthis": "^1.0.3",
|
||||
"graphql": "^16.8.0",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-language-service-interface": "^2.9.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"insomnia-importers": "^3.6.0",
|
||||
"io-ts": "^2.2.20",
|
||||
"httpsnippet": "^2.0.0",
|
||||
"insomnia-importers": "^3.3.0",
|
||||
"io-ts": "^2.2.16",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"jsonpath-plus": "^7.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lossless-json": "^2.0.11",
|
||||
"lossless-json": "^2.0.8",
|
||||
"minisearch": "^6.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
"postman-collection": "^4.2.0",
|
||||
"postman-collection": "^4.1.4",
|
||||
"process": "^0.11.10",
|
||||
"qs": "^6.11.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"qs": "^6.10.3",
|
||||
"rxjs": "^7.5.5",
|
||||
"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-v4": "npm:socket.io-client@^4.4.1",
|
||||
"socketio-wildcard": "^2.0.0",
|
||||
"splitpanes": "^3.1.5",
|
||||
"splitpanes": "^3.1.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"tern": "^0.24.3",
|
||||
"timers": "^0.1.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"url": "^0.11.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vue": "^3.3.4",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.25",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-pdf-embed": "^1.1.6",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-tippy": "6.3.1",
|
||||
"vue-pdf-embed": "^1.1.4",
|
||||
"vue-router": "^4.0.16",
|
||||
"vue-tippy": "6.0.0-alpha.58",
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^6.3.4",
|
||||
"workbox-window": "^7.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"wonka": "^4.0.15",
|
||||
"workbox-window": "^6.5.4",
|
||||
"xml-formatter": "^3.4.1",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@iconify-json/lucide": "^1.1.119",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||
"@graphql-codegen/add": "^3.2.0",
|
||||
"@graphql-codegen/cli": "^2.8.0",
|
||||
"@graphql-codegen/typed-document-node": "^2.3.1",
|
||||
"@graphql-codegen/typescript": "^2.7.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
|
||||
"@graphql-codegen/urql-introspection": "^2.2.0",
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@iconify-json/lucide": "^1.1.109",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/har-format": "^1.2.12",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/lossless-json": "^1.0.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/paho-mqtt": "^1.0.7",
|
||||
"@types/paho-mqtt": "^1.0.6",
|
||||
"@types/postman-collection": "^3.5.7",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/yargs-parser": "^21.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||
"@typescript-eslint/parser": "^6.4.0",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/runtime-core": "^3.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vue/compiler-sfc": "^3.2.39",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"@vue/runtime-core": "^3.2.39",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.1.3",
|
||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||
"sass": "^1.66.0",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-fonts": "^1.0.3",
|
||||
"unplugin-icons": "^0.16.5",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-checker": "^0.6.1",
|
||||
"vite-plugin-html-config": "^1.0.11",
|
||||
"vite-plugin-inspect": "^0.7.38",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vite-plugin-windicss": "^1.9.1",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8",
|
||||
"openapi-types": "^12.0.0",
|
||||
"rollup-plugin-polyfill-node": "^0.10.1",
|
||||
"sass": "^1.53.0",
|
||||
"typescript": "^4.5.4",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
"unplugin-vue-components": "^0.21.0",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-checker": "^0.5.1",
|
||||
"vite-plugin-fonts": "^0.6.0",
|
||||
"vite-plugin-html-config": "^1.0.10",
|
||||
"vite-plugin-inspect": "^0.7.4",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.5",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vitest": "^0.32.2",
|
||||
"vue-tsc": "^0.38.2",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#6366f1" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>
|
||||
|
Before Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 926 KiB After Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 358 KiB |
|
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 382 KiB |
@@ -18,7 +18,6 @@ import { HOPP_MODULES } from "@modules/."
|
||||
import { isLoadingInitialRoute } from "@modules/router"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
|
||||
import { platform } from "./platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -46,5 +45,4 @@ if (APP_IS_IN_DEV_MODE) {
|
||||
|
||||
// Run module root component setup code
|
||||
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
|
||||
platform.addedHoppModules?.forEach((mod) => mod.onRootSetup?.())
|
||||
</script>
|
||||
|
||||
39
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
@@ -14,7 +14,6 @@ declare module 'vue' {
|
||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.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']
|
||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||
@@ -24,14 +23,10 @@ declare module 'vue' {
|
||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.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']
|
||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||
@@ -73,18 +68,12 @@ declare module 'vue' {
|
||||
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
||||
GraphqlAuthorization: typeof import('./components/graphql/Authorization.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']
|
||||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
|
||||
GraphqlResponse: typeof import('./components/graphql/Response.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']
|
||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||
GraphqlVariable: typeof import('./components/graphql/Variable.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']
|
||||
@@ -96,22 +85,17 @@ declare module 'vue' {
|
||||
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']
|
||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||
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']
|
||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||
@@ -133,17 +117,14 @@ declare module 'vue' {
|
||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||
HttpTabHead: typeof import('./components/http/TabHead.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']
|
||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['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']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
@@ -156,8 +137,6 @@ declare module 'vue' {
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['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']
|
||||
@@ -177,8 +156,6 @@ declare module 'vue' {
|
||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.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']
|
||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||
@@ -190,7 +167,6 @@ declare module 'vue' {
|
||||
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']
|
||||
@@ -205,8 +181,8 @@ declare module 'vue' {
|
||||
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('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
||||
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.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']
|
||||
@@ -222,4 +198,5 @@ declare module 'vue' {
|
||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,53 +2,16 @@
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
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()
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const showShortcuts = ref(false)
|
||||
const showShare = 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", () => {
|
||||
showShortcuts.value = !showShortcuts.value
|
||||
})
|
||||
@@ -60,9 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
|
||||
defineActionHandler("modals.login.toggle", () => {
|
||||
showLogin.value = !showLogin.value
|
||||
})
|
||||
|
||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||
teamID.value = teamId
|
||||
confirmRemove.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!--
|
||||
<HoppSmartItem
|
||||
ref="chat"
|
||||
:icon="IconMessageCircle"
|
||||
@@ -89,34 +88,20 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
-->
|
||||
<template
|
||||
v-for="footerItem in platform.ui?.additionalFooterMenuItems"
|
||||
:key="footerItem.id"
|
||||
>
|
||||
<template v-if="footerItem.action.type === 'link'">
|
||||
<HoppSmartItem
|
||||
:icon="footerItem.icon"
|
||||
:label="footerItem.text(t)"
|
||||
:to="footerItem.action.href"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
</template>
|
||||
<HoppSmartItem
|
||||
v-else
|
||||
:icon="footerItem.icon"
|
||||
:label="footerItem.text(t)"
|
||||
blank
|
||||
@click="
|
||||
() => {
|
||||
// @ts-expect-error TypeScript not understanding the type
|
||||
footerItem.action.do()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="`${t('app.whats_new')}`"
|
||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconActivity"
|
||||
:label="t('app.status')"
|
||||
to="https://status.hoppscotch.io"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
@@ -222,11 +207,15 @@ import IconColumns from "~icons/lucide/columns"
|
||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||
import IconShieldCheck from "~icons/lucide/shield-check"
|
||||
import IconBook from "~icons/lucide/book"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
import IconActivity from "~icons/lucide/activity"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import IconTwitter from "~icons/lucide/twitter"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import { showChat } from "@modules/crisp"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
@@ -273,6 +262,10 @@ const nativeShare = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const chatWithUs = () => {
|
||||
showChat()
|
||||
}
|
||||
|
||||
const showDeveloperOptionModal = () => {
|
||||
if (currentUser.value) {
|
||||
showDeveloperOptions.value = true
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
</div>
|
||||
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
||||
<button
|
||||
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"
|
||||
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"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
>
|
||||
<span class="inline-flex flex-1 items-center">
|
||||
<icon-lucide-search class="mr-2 svg-icons" />
|
||||
{{ t("app.search") }}
|
||||
</span>
|
||||
<span class="flex space-x-1">
|
||||
<span class="flex">
|
||||
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
|
||||
<kbd class="shortcut-key">K</kbd>
|
||||
</span>
|
||||
@@ -254,10 +254,8 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { useToast } from "~/composables/toast"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
/**
|
||||
* Once the PWA code is initialized, this holds a method
|
||||
@@ -374,8 +372,6 @@ const handleTeamEdit = () => {
|
||||
editingTeamID.value = workspace.value.teamID
|
||||
editingTeamName.value = { name: selectedTeam.value.name }
|
||||
displayModalEdit(true)
|
||||
} else {
|
||||
noPermission()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,19 +382,6 @@ const settings = ref<any | null>(null)
|
||||
const logout = 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(
|
||||
"user.login",
|
||||
() => {
|
||||
@@ -406,8 +389,4 @@ defineActionHandler(
|
||||
},
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
|
||||
const noPermission = () => {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<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>
|
||||
@@ -8,41 +8,91 @@
|
||||
{{ t("settings.interceptor_description") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
v-for="interceptor in interceptors"
|
||||
:key="interceptor.interceptorID"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<HoppSmartRadio
|
||||
:value="interceptor.interceptorID"
|
||||
:label="unref(interceptor.name(t))"
|
||||
:selected="interceptorSelection === interceptor.interceptorID"
|
||||
@change="interceptorSelection = interceptor.interceptorID"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="interceptor.selectorSubtitle"
|
||||
v-if="interceptor.selectorSubtitle"
|
||||
/>
|
||||
</div>
|
||||
<HoppSmartRadioGroup
|
||||
v-model="interceptorSelection"
|
||||
:radios="interceptors"
|
||||
/>
|
||||
<div
|
||||
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
||||
class="flex space-x-2"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||
blank
|
||||
:icon="IconChrome"
|
||||
label="Chrome"
|
||||
outline
|
||||
class="!flex-1"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||
blank
|
||||
:icon="IconFirefox"
|
||||
label="Firefox"
|
||||
outline
|
||||
class="!flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { useService } from "dioc/vue"
|
||||
import { Ref, unref } from "vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const interceptorService = useService(InterceptorService)
|
||||
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
||||
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
||||
|
||||
const interceptorSelection =
|
||||
interceptorService.currentInterceptorID as Ref<string>
|
||||
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
||||
|
||||
const interceptors = interceptorService.availableInterceptors
|
||||
const extensionVersion = computed(() => {
|
||||
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>
|
||||
|
||||
@@ -30,52 +30,134 @@
|
||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||
{{ t("support.title") }}
|
||||
</h2>
|
||||
<template
|
||||
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||
:key="item.id"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-if="item.action.type === 'link'"
|
||||
:icon="item.icon"
|
||||
:label="item.text(t)"
|
||||
:to="item.action.href"
|
||||
:description="item.subtitle(t)"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.text(t)"
|
||||
:description="item.subtitle(t)"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="
|
||||
() => {
|
||||
// @ts-expect-error Typescript isn't able to understand
|
||||
item.action.do()
|
||||
hideModal()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppSmartItem
|
||||
:icon="IconBook"
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
:description="t('support.documentation')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="t('app.whats_new')"
|
||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
||||
:description="t('support.changelog')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconActivity"
|
||||
:label="t('app.status')"
|
||||
to="https://status.hoppscotch.io"
|
||||
blank
|
||||
:description="t('app.status_description')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconLock"
|
||||
:label="`${t('app.terms_and_privacy')}`"
|
||||
to="https://docs.hoppscotch.io/support/privacy"
|
||||
blank
|
||||
:description="t('app.terms_and_privacy')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||
{{ t("settings.follow") }}
|
||||
</h2>
|
||||
<HoppSmartItem
|
||||
:icon="IconDiscord"
|
||||
:label="t('app.discord')"
|
||||
to="https://hoppscotch.io/discord"
|
||||
blank
|
||||
:description="t('app.join_discord_community')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconTwitter"
|
||||
:label="t('app.twitter')"
|
||||
to="https://hoppscotch.io/twitter"
|
||||
blank
|
||||
:description="t('support.twitter')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="`${t('app.github')}`"
|
||||
to="https://github.com/hoppscotch/hoppscotch"
|
||||
blank
|
||||
:description="t('support.github')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconMessageCircle"
|
||||
:label="t('app.chat_with_us')"
|
||||
:description="t('support.chat')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="chatWithUs()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconUserPlus"
|
||||
:label="`${t('app.invite')}`"
|
||||
:description="t('shortcut.miscellaneous.invite')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="expandInvite()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="navigatorShare"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconShare2"
|
||||
:label="`${t('request.share')}`"
|
||||
:description="t('request.share_description')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="nativeShare()"
|
||||
/>
|
||||
</div>
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from "vue"
|
||||
import { ref, watch } from "vue"
|
||||
import IconSidebar from "~icons/lucide/sidebar"
|
||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||
import IconBook from "~icons/lucide/book"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
import IconActivity from "~icons/lucide/activity"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconDiscord from "~icons/brands/discord"
|
||||
import IconTwitter from "~icons/brands/twitter"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { showChat } from "@modules/crisp"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const navigatorShare = !!navigator.share
|
||||
const showShare = ref(false)
|
||||
|
||||
const ZEN_MODE = useSetting("ZEN_MODE")
|
||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||
@@ -92,10 +174,19 @@ defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
defineActionHandler("modals.share.toggle", () => {
|
||||
showShare.value = !showShare.value
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const chatWithUs = () => {
|
||||
showChat()
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const expandNavigation = () => {
|
||||
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
|
||||
hideModal()
|
||||
@@ -106,6 +197,24 @@ const expandCollection = () => {
|
||||
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 = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
@@ -18,18 +18,13 @@
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
@resize="setPaneEvent($event, 'horizontal')"
|
||||
>
|
||||
<Pane
|
||||
:size="PANE_MAIN_TOP_SIZE"
|
||||
class="flex flex-col !overflow-auto"
|
||||
min-size="25"
|
||||
>
|
||||
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
|
||||
<slot name="primary" />
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="hasSecondary"
|
||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||
class="flex flex-col !overflow-auto"
|
||||
min-size="25"
|
||||
>
|
||||
<slot name="secondary" />
|
||||
</Pane>
|
||||
@@ -38,7 +33,7 @@
|
||||
<Pane
|
||||
v-if="SIDEBAR && hasSidebar"
|
||||
:size="PANE_SIDEBAR_SIZE"
|
||||
min-size="25"
|
||||
min-size="20"
|
||||
class="flex flex-col !overflow-auto bg-primaryContrast"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
@@ -83,10 +78,10 @@ type PaneEvent = {
|
||||
size: number
|
||||
}
|
||||
|
||||
const PANE_MAIN_SIZE = ref(70)
|
||||
const PANE_SIDEBAR_SIZE = ref(30)
|
||||
const PANE_MAIN_TOP_SIZE = ref(35)
|
||||
const PANE_MAIN_BOTTOM_SIZE = ref(65)
|
||||
const PANE_MAIN_SIZE = ref(74)
|
||||
const PANE_SIDEBAR_SIZE = ref(26)
|
||||
const PANE_MAIN_TOP_SIZE = ref(42)
|
||||
const PANE_MAIN_BOTTOM_SIZE = ref(58)
|
||||
|
||||
if (!COLUMN_LAYOUT.value) {
|
||||
PANE_MAIN_TOP_SIZE.value = 50
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
styles="px-6 py-4 border-b border-dividerLight"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
input-styles="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
||||
/>
|
||||
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col divide-y divide-dividerLight">
|
||||
<HoppSmartPlaceholder
|
||||
@@ -19,7 +21,6 @@
|
||||
>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<details
|
||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||
v-else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||
<div class="flex mb-4 space-x-2">
|
||||
<div class="flex pb-4 my-4 space-x-2">
|
||||
<div class="flex flex-col items-end space-y-4 text-right">
|
||||
<span class="flex items-center flex-1">
|
||||
{{ t("shortcut.request.send_request") }}
|
||||
|
||||
@@ -8,46 +8,89 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<template
|
||||
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||
:key="item.id"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-if="item.action.type === 'link'"
|
||||
:icon="item.icon"
|
||||
:label="item.text(t)"
|
||||
:to="item.action.href"
|
||||
:description="item.subtitle(t)"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.text(t)"
|
||||
:description="item.subtitle(t)"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="
|
||||
() => {
|
||||
// @ts-expect-error Typescript isn't able to understand
|
||||
item.action.do()
|
||||
hideModal()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppSmartItem
|
||||
:icon="IconBook"
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
:description="t('support.documentation')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconZap"
|
||||
:label="t('app.keyboard_shortcuts')"
|
||||
:description="t('support.shortcuts')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="showShortcuts()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="t('app.whats_new')"
|
||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
||||
:description="t('support.changelog')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconMessageCircle"
|
||||
:label="t('app.chat_with_us')"
|
||||
:description="t('support.chat')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="chatWithUs()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconGitHub"
|
||||
:label="t('app.github')"
|
||||
to="https://hoppscotch.io/github"
|
||||
blank
|
||||
:description="t('support.github')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconDiscord"
|
||||
:label="t('app.join_discord_community')"
|
||||
to="https://hoppscotch.io/discord"
|
||||
blank
|
||||
:description="t('support.community')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconTwitter"
|
||||
:label="t('app.twitter')"
|
||||
to="https://hoppscotch.io/twitter"
|
||||
blank
|
||||
:description="t('support.twitter')"
|
||||
:info-icon="IconChevronRight"
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTwitter from "~icons/brands/twitter"
|
||||
import IconDiscord from "~icons/brands/discord"
|
||||
import IconGitHub from "~icons/lucide/github"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
import IconBook from "~icons/lucide/book"
|
||||
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import { showChat } from "@modules/crisp"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -59,6 +102,16 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const chatWithUs = () => {
|
||||
showChat()
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const showShortcuts = () => {
|
||||
invokeAction("flyouts.keybinds.toggle")
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
@@ -80,11 +80,10 @@ const props = defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
|
||||
const formattedShortcutKeys = computed(
|
||||
() =>
|
||||
props.entry.meta?.keyboardShortcut?.map((key) => {
|
||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||
})
|
||||
const formattedShortcutKeys = computed(() =>
|
||||
props.entry.meta?.keyboardShortcut?.map((key) => {
|
||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||
})
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<IconLucideCheckCircle class="text-accent" />
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
<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>
|
||||
@@ -95,23 +95,6 @@ import {
|
||||
import { isEqual } from "lodash-es"
|
||||
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.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()
|
||||
|
||||
@@ -127,19 +110,6 @@ const spotlightService = useService(SpotlightService)
|
||||
|
||||
useService(HistorySpotlightSearcherService)
|
||||
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("")
|
||||
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addNewCollection"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewCollection"
|
||||
/>
|
||||
<label for="selectLabelAdd">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -57,28 +65,28 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addNewCollection = () => {
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(t("collection.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", editingName.value)
|
||||
emit("submit", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="addFolder"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAddFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addFolder"
|
||||
/>
|
||||
<label for="selectLabelAddFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -57,27 +65,27 @@ const emit = defineEmits<{
|
||||
(e: "add-folder", name: string): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addFolder = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
if (name.value.trim() === "") {
|
||||
toast.error(t("folder.invalid_name"))
|
||||
return
|
||||
}
|
||||
emit("add-folder", editingName.value)
|
||||
emit("add-folder", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,19 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addRequest"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAddRequest"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addRequest"
|
||||
/>
|
||||
<label for="selectLabelAddRequest">{{ t("action.label") }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -58,23 +64,23 @@ const emit = defineEmits<{
|
||||
(event: "add-request", name: string): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = currentActiveTab.value.document.request.name
|
||||
name.value = currentActiveTab.value.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addRequest = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
if (name.value.trim() === "") {
|
||||
toast.error(`${t("error.empty_req_name")}`)
|
||||
return
|
||||
}
|
||||
emit("add-request", editingName.value)
|
||||
emit("add-request", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="saveCollection"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveCollection"
|
||||
/>
|
||||
<label for="selectLabelEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -59,26 +67,26 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingCollectionName,
|
||||
(newName) => {
|
||||
editingName.value = newName
|
||||
name.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
const saveCollection = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
if (name.value.trim() === "") {
|
||||
toast.error(t("collection.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", editingName.value)
|
||||
emit("submit", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editFolder"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEditFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editFolder"
|
||||
/>
|
||||
<label for="selectLabelEditFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -59,26 +67,26 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingFolderName,
|
||||
(newName) => {
|
||||
editingName.value = newName
|
||||
name.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
const editFolder = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
if (name.value.trim() === "") {
|
||||
toast.error(t("folder.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", editingName.value)
|
||||
emit("submit", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editRequest"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEditReq"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editRequest"
|
||||
/>
|
||||
<label for="selectLabelEditReq">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -60,19 +68,19 @@ const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const editingName = useVModel(props, "modelValue")
|
||||
const name = useVModel(props, "modelValue")
|
||||
|
||||
const editRequest = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
if (name.value.trim() === "") {
|
||||
toast.error(t("request.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", editingName.value)
|
||||
emit("submit", name.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<HoppSmartTree :adapter="myAdapter">
|
||||
<SmartTree :adapter="myAdapter">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</SmartTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -303,10 +303,7 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -8,15 +8,21 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartInput
|
||||
v-model="requestName"
|
||||
styles="relative flex"
|
||||
placeholder=" "
|
||||
:label="t('request.name')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveRequestAs"
|
||||
/>
|
||||
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelSaveReq"
|
||||
v-model="requestName"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequestAs"
|
||||
/>
|
||||
<label for="selectLabelSaveReq">
|
||||
{{ t("request.name") }}
|
||||
</label>
|
||||
</div>
|
||||
<label class="p-4">
|
||||
{{ t("collection.select_location") }}
|
||||
</label>
|
||||
@@ -56,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue"
|
||||
import { nextTick, reactive, ref, watch } from "vue"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
HoppGQLRequest,
|
||||
@@ -71,6 +77,7 @@ import {
|
||||
updateTeamRequest,
|
||||
} from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
@@ -81,9 +88,8 @@ import {
|
||||
} from "~/newstore/collections"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
|
||||
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -101,12 +107,10 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
mode: "rest" | "graphql"
|
||||
request?: HoppRESTRequest | HoppGQLRequest | null
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
mode: "rest",
|
||||
request: null,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -122,36 +126,22 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const gqlRequestName = computedWithControl(
|
||||
() => activeGQLTab.value,
|
||||
() => activeGQLTab.value.document.request.name
|
||||
)
|
||||
|
||||
const gqlRequestName = useGQLRequestName()
|
||||
const restRequestName = computedWithControl(
|
||||
() => activeRESTTab.value,
|
||||
() => activeRESTTab.value.document.request.name
|
||||
() => currentActiveTab.value,
|
||||
() => currentActiveTab.value.document.request.name
|
||||
)
|
||||
|
||||
const reqName = computed(() => {
|
||||
if (props.request) {
|
||||
return props.request.name
|
||||
} else if (props.mode === "rest") {
|
||||
return restRequestName.value
|
||||
} else {
|
||||
return gqlRequestName.value
|
||||
}
|
||||
})
|
||||
|
||||
const requestName = ref(reqName.value)
|
||||
const requestName = ref(
|
||||
props.mode === "rest" ? restRequestName.value : gqlRequestName.value
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [activeRESTTab.value, activeGQLTab.value],
|
||||
() => [currentActiveTab.value, gqlRequestName.value],
|
||||
() => {
|
||||
if (props.mode === "rest") {
|
||||
requestName.value = activeRESTTab.value?.document.request.name ?? ""
|
||||
} else {
|
||||
requestName.value = activeGQLTab.value?.document.request.name ?? ""
|
||||
}
|
||||
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
||||
} else requestName.value = gqlRequestName.value
|
||||
}
|
||||
)
|
||||
|
||||
@@ -210,8 +200,8 @@ const saveRequestAs = async () => {
|
||||
|
||||
const requestUpdated =
|
||||
props.mode === "rest"
|
||||
? cloneDeep(activeRESTTab.value.document.request)
|
||||
: cloneDeep(activeGQLTab.value.document.request)
|
||||
? cloneDeep(currentActiveTab.value.document.request)
|
||||
: cloneDeep(getGQLSession().request)
|
||||
|
||||
requestUpdated.name = requestName.value
|
||||
|
||||
@@ -224,7 +214,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -251,7 +241,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -279,7 +269,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -439,7 +429,7 @@ const updateTeamCollectionOrFolder = (
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -460,7 +450,7 @@ const updateTeamCollectionOrFolder = (
|
||||
const requestSaved = () => {
|
||||
toast.success(`${t("request.added")}`)
|
||||
nextTick(() => {
|
||||
activeRESTTab.value.document.isDirty = false
|
||||
currentActiveTab.value.document.isDirty = false
|
||||
})
|
||||
hideModal()
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<HoppSmartTree :adapter="teamAdapter">
|
||||
<SmartTree :adapter="teamAdapter">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
@@ -311,7 +311,7 @@
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</SmartTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -326,10 +326,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="addNewCollection"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewCollection"
|
||||
/>
|
||||
<label for="selectLabelGqlAdd">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addFolder"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAddFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addFolder"
|
||||
/>
|
||||
<label for="selectLabelGqlAddFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addRequest"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAddRequest"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addRequest"
|
||||
/>
|
||||
<label for="selectLabelGqlAddRequest">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -36,7 +44,7 @@
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { getGQLSession } from "~/newstore/GQLSession"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -57,24 +65,24 @@ const emit = defineEmits<{
|
||||
): void
|
||||
}>()
|
||||
|
||||
const editingName = ref("")
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = currentActiveTab.value?.document.request.name
|
||||
name.value = getGQLSession().request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addRequest = () => {
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("error.empty_req_name")}`)
|
||||
return
|
||||
}
|
||||
emit("add-request", {
|
||||
name: editingName.value,
|
||||
name: name.value,
|
||||
path: props.folderPath,
|
||||
})
|
||||
hideModal()
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
@click="
|
||||
emit('add-request', {
|
||||
path: `${collectionIndex}`,
|
||||
index: collection.requests.length,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -220,7 +219,6 @@ import {
|
||||
moveGraphqlRequest,
|
||||
} from "~/newstore/collections"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
@@ -295,22 +293,6 @@ const removeCollection = () => {
|
||||
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)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveCollection"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveCollection"
|
||||
/>
|
||||
<label for="selectLabelGqlEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -52,17 +60,17 @@ const emit = defineEmits<{
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const editingName = ref<string | null>()
|
||||
const name = ref<string | null>()
|
||||
|
||||
watch(
|
||||
() => props.editingCollectionName,
|
||||
(val) => {
|
||||
editingName.value = val
|
||||
name.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const saveCollection = () => {
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -70,7 +78,7 @@ const saveCollection = () => {
|
||||
// TODO: Better typechecking here ?
|
||||
const collectionUpdated = {
|
||||
...(props.editingCollection as any),
|
||||
name: editingName.value,
|
||||
name: name.value,
|
||||
}
|
||||
|
||||
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
|
||||
@@ -78,7 +86,7 @@ const saveCollection = () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editFolder"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEditFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editFolder"
|
||||
/>
|
||||
<label for="selectLabelGqlEditFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
v-model="requestUpdateData.name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveRequest"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEditReq"
|
||||
v-model="requestUpdateData.name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequest"
|
||||
/>
|
||||
<label for="selectLabelGqlEditReq">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -34,12 +34,7 @@
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="
|
||||
emit('add-request', {
|
||||
path: folderPath,
|
||||
index: folder.requests.length,
|
||||
})
|
||||
"
|
||||
@click="emit('add-request', { path: folderPath })"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -203,7 +198,6 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||
import { computed, ref } from "vue"
|
||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -255,8 +249,10 @@ const collectionIcon = computed(() => {
|
||||
|
||||
const pick = () => {
|
||||
emit("select", {
|
||||
pickedType: "gql-my-folder",
|
||||
folderPath: props.folderPath,
|
||||
picked: {
|
||||
pickedType: "gql-my-folder",
|
||||
folderPath: props.folderPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -277,22 +273,6 @@ const removeFolder = () => {
|
||||
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)
|
||||
toast.success(t("state.deleted"))
|
||||
}
|
||||
|
||||
@@ -20,28 +20,22 @@
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</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>
|
||||
<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>
|
||||
<tippy
|
||||
ref="options"
|
||||
@@ -127,6 +121,7 @@
|
||||
<script setup lang="ts">
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFile from "~icons/lucide/file"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
@@ -137,12 +132,7 @@ import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||
import {
|
||||
createNewTab,
|
||||
getTabRefWithSaveContext,
|
||||
currentTabID,
|
||||
currentActiveTab,
|
||||
} from "~/helpers/graphql/tab"
|
||||
import { setGQLSession } from "~/newstore/GQLSession"
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
@@ -164,18 +154,6 @@ const props = defineProps({
|
||||
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
|
||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||
|
||||
@@ -201,24 +179,7 @@ const selectRequest = () => {
|
||||
if (props.saveRequest) {
|
||||
pick()
|
||||
} else {
|
||||
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,
|
||||
},
|
||||
setGQLSession({
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: props.request.name,
|
||||
@@ -229,7 +190,8 @@ const selectRequest = () => {
|
||||
auth: props.request.auth,
|
||||
})
|
||||
),
|
||||
isDirty: false,
|
||||
schema: "",
|
||||
response: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -252,18 +214,6 @@ const removeRequest = () => {
|
||||
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)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="py-2 pl-4 pr-2 bg-transparent !border-0"
|
||||
class="py-2 pl-4 pr-2 bg-transparent"
|
||||
/>
|
||||
<div
|
||||
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
||||
@@ -137,6 +137,7 @@ import {
|
||||
addGraphqlFolder,
|
||||
saveGraphqlRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
@@ -145,7 +146,6 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -265,22 +265,17 @@ export default defineComponent({
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.displayModalEdit(true)
|
||||
},
|
||||
onAddRequest({ name, path, index }) {
|
||||
onAddRequest({ name, path }) {
|
||||
const newRequest = {
|
||||
...currentActiveTab.value.document.request,
|
||||
...getGQLSession().request,
|
||||
name,
|
||||
}
|
||||
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
|
||||
createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: index,
|
||||
},
|
||||
setGQLSession({
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
schema: "",
|
||||
response: "",
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
"
|
||||
>
|
||||
<WorkspaceCurrent :section="t('tab.collections')" />
|
||||
|
||||
<HoppSmartInput
|
||||
<input
|
||||
v-model="filterTexts"
|
||||
:placeholder="t('action.search')"
|
||||
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
|
||||
type="search"
|
||||
:autofocus="false"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="py-2 pl-4 pr-2 bg-transparent"
|
||||
:disabled="collectionsType.type === 'team-collections'"
|
||||
/>
|
||||
</div>
|
||||
@@ -239,7 +238,6 @@ import {
|
||||
resetTeamRequestsContext,
|
||||
} from "~/helpers/collection/collection"
|
||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -2068,8 +2066,4 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler("collection.new", () => {
|
||||
displayModalAdd(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
t("environment.name")
|
||||
}}</label>
|
||||
<input
|
||||
v-model="editingName"
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="t('environment.variable')"
|
||||
class="input"
|
||||
@@ -21,12 +21,7 @@
|
||||
<label for="value" class="font-semibold min-w-10">{{
|
||||
t("environment.value")
|
||||
}}</label>
|
||||
<input
|
||||
v-model="editingValue"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('environment.value')"
|
||||
/>
|
||||
<input type="text" :value="value" class="input" />
|
||||
</div>
|
||||
<div class="flex items-center space-x-8 ml-2">
|
||||
<label for="scope" class="font-semibold min-w-10">
|
||||
@@ -93,6 +88,7 @@ const props = defineProps<{
|
||||
position: { top: number; left: number }
|
||||
name: string
|
||||
value: string
|
||||
replaceWithVariable: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -110,12 +106,9 @@ watch(
|
||||
scope.value = {
|
||||
type: "global",
|
||||
}
|
||||
name.value = ""
|
||||
replaceWithVariable.value = false
|
||||
editingName.value = ""
|
||||
editingValue.value = ""
|
||||
}
|
||||
editingName.value = props.name
|
||||
editingValue.value = props.value
|
||||
}
|
||||
)
|
||||
|
||||
@@ -139,32 +132,31 @@ const scope = ref<Scope>({
|
||||
|
||||
const replaceWithVariable = ref(false)
|
||||
|
||||
const editingName = ref(props.name)
|
||||
const editingValue = ref(props.value)
|
||||
const name = ref("")
|
||||
|
||||
const addEnvironment = async () => {
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
if (scope.value.type === "global") {
|
||||
addGlobalEnvVariable({
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
key: name.value,
|
||||
value: props.value,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else if (scope.value.type === "my-environment") {
|
||||
addEnvironmentVariable(scope.value.index, {
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
key: name.value,
|
||||
value: props.value,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else {
|
||||
const newVariables = [
|
||||
...scope.value.environment.environment.variables,
|
||||
{
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
key: name.value,
|
||||
value: props.value,
|
||||
},
|
||||
]
|
||||
await pipe(
|
||||
@@ -187,11 +179,11 @@ const addEnvironment = async () => {
|
||||
}
|
||||
if (replaceWithVariable.value) {
|
||||
//replace the current tab endpoint with the variable name with << and >>
|
||||
const variableName = `<<${editingName.value}>>`
|
||||
const variableName = `<<${name.value}>>`
|
||||
//replace the currenttab endpoint containing the value in the text with variablename
|
||||
currentActiveTab.value.document.request.endpoint =
|
||||
currentActiveTab.value.document.request.endpoint.replace(
|
||||
editingValue.value,
|
||||
props.value,
|
||||
variableName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,302 +1,160 @@
|
||||
<template>
|
||||
<div class="flex divide-x divide-dividerLight">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => envSelectorActions!.focus()"
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent select-wrapper"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="select-wrapper"
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLayers"
|
||||
:label="
|
||||
mdAndLarger
|
||||
? selectedEnv.type !== 'NO_ENV_SELECTED'
|
||||
? selectedEnv.name
|
||||
: `${t('environment.select')}`
|
||||
: ''
|
||||
"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLayers"
|
||||
:label="
|
||||
mdAndLarger
|
||||
? selectedEnv.type !== 'NO_ENV_SELECTED'
|
||||
? selectedEnv.name
|
||||
: `${t('environment.select')}`
|
||||
: ''
|
||||
<HoppSmartItem
|
||||
v-if="!isScopeSelector"
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="envSelectorActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
<HoppSmartItem
|
||||
v-else-if="isScopeSelector && modelValue"
|
||||
:label="t('environment.global')"
|
||||
:icon="IconGlobe"
|
||||
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
|
||||
:active-info-icon="modelValue.type === 'global'"
|
||||
@click="
|
||||
() => {
|
||||
$emit('update:modelValue', {
|
||||
type: 'global',
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedEnvTab"
|
||||
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-if="!isScopeSelector"
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-else-if="isScopeSelector && modelValue"
|
||||
:label="t('environment.global')"
|
||||
:icon="IconGlobe"
|
||||
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
|
||||
:active-info-icon="modelValue.type === 'global'"
|
||||
@click="
|
||||
() => {
|
||||
$emit('update:modelValue', {
|
||||
type: 'global',
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedEnvTab"
|
||||
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
|
||||
!isTeamSelected || workspace.type === 'personal'
|
||||
? 'bg-primaryLight'
|
||||
: ''
|
||||
}`"
|
||||
render-inactive-tabs
|
||||
<HoppSmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_environments')}`"
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_environments')}`"
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.name"
|
||||
:info-icon="isEnvActive(index) ? IconCheck : undefined"
|
||||
:active-info-icon="isEnvActive(index)"
|
||||
@click="
|
||||
() => {
|
||||
handleEnvironmentChange(index, {
|
||||
type: 'my-environment',
|
||||
environment: gen,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="myEnvironments.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
:label="`${t('environment.team_environments')}`"
|
||||
:disabled="!isTeamSelected || workspace.type === 'personal'"
|
||||
>
|
||||
<div
|
||||
v-if="teamListLoading"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-else-if="isTeamSelected" class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.name"
|
||||
:info-icon="isEnvActive(index) ? IconCheck : undefined"
|
||||
:active-info-icon="isEnvActive(index)"
|
||||
:label="gen.environment.name"
|
||||
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
|
||||
:active-info-icon="isEnvActive(gen.id)"
|
||||
@click="
|
||||
() => {
|
||||
handleEnvironmentChange(index, {
|
||||
type: 'my-environment',
|
||||
type: 'team-environment',
|
||||
environment: gen,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="myEnvironments.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
:label="`${t('environment.team_environments')}`"
|
||||
:disabled="!isTeamSelected || workspace.type === 'personal'"
|
||||
>
|
||||
<div
|
||||
v-if="teamListLoading"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">
|
||||
{{ t("state.loading") }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isTeamSelected" class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.environment.name"
|
||||
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
|
||||
:active-info-icon="isEnvActive(gen.id)"
|
||||
@click="
|
||||
() => {
|
||||
handleEnvironmentChange(index, {
|
||||
type: 'team-environment',
|
||||
environment: gen,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(teamAdapterError) }}
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<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>
|
||||
</HoppSmartPlaceholder>
|
||||
</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,
|
||||
}"
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
{{ t("environment.list") }}
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="!selectedEnv.variables"
|
||||
:title="t('action.edit')"
|
||||
:icon="IconEdit"
|
||||
@click="
|
||||
() => {
|
||||
editEnv()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(teamAdapterError) }}
|
||||
</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>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { computed, onMounted, ref, watch } from "vue"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconLayers from "~icons/lucide/layers"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconGlobe from "~icons/lucide/globe"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
@@ -304,7 +162,6 @@ import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { useReadonlyStream, useStream } from "~/composables/stream"
|
||||
import {
|
||||
environments$,
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
@@ -312,14 +169,12 @@ import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { onMounted } from "vue"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
type Scope =
|
||||
| {
|
||||
@@ -334,10 +189,12 @@ type Scope =
|
||||
type: "team-environment"
|
||||
environment: TeamEnvironment
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isScopeSelector?: boolean
|
||||
modelValue?: Scope
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", data: Scope): void
|
||||
}>()
|
||||
@@ -373,6 +230,7 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||
type: "team",
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
@@ -395,6 +253,32 @@ 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 = (
|
||||
index: number,
|
||||
env?:
|
||||
@@ -436,6 +320,7 @@ const handleEnvironmentChange = (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isEnvActive = (id: string | number) => {
|
||||
if (props.isScopeSelector) {
|
||||
if (props.modelValue?.type === "my-environment") {
|
||||
@@ -459,32 +344,6 @@ 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(() => {
|
||||
if (props.isScopeSelector) {
|
||||
if (props.modelValue?.type === "my-environment") {
|
||||
@@ -504,13 +363,10 @@ const selectedEnv = computed(() => {
|
||||
}
|
||||
} else {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
const environment =
|
||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.value.index,
|
||||
name: environment.name,
|
||||
variables: environment.variables,
|
||||
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
|
||||
}
|
||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find(
|
||||
@@ -524,7 +380,6 @@ const selectedEnv = computed(() => {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
variables: teamEnv.environment.variables,
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
@@ -574,8 +429,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// Template refs
|
||||
const envSelectorActions = ref<TippyComponent | null>(null)
|
||||
const envQuickPeekActions = ref<TippyComponent | null>(null)
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
@@ -589,32 +443,4 @@ 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>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
@edit-environment="editEnvironment('Global')"
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsMy v-show="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsTeams
|
||||
v-show="environmentType.type === 'team-environments'"
|
||||
v-if="environmentType.type === 'team-environments'"
|
||||
:team="environmentType.selectedTeam"
|
||||
:team-environments="teamEnvironmentList"
|
||||
:loading="loading"
|
||||
@@ -34,13 +34,6 @@
|
||||
@hide-modal="displayModalNew(false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="showConfirmRemoveEnvModal"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||
@resolve="removeSelectedEnvironment()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -51,7 +44,6 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import {
|
||||
getSelectedEnvironmentIndex,
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
@@ -62,15 +54,8 @@ import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { deleteEnvironment } from "~/newstore/environments"
|
||||
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import { useToast } from "~/composables/toast"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
type EnvironmentType = "my-environments" | "team-environments"
|
||||
|
||||
@@ -183,7 +168,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const showConfirmRemoveEnvModal = ref(false)
|
||||
const showModalNew = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
@@ -210,47 +194,14 @@ const editEnvironment = (environmentIndex: "Global") => {
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
const removeSelectedEnvironment = () => {
|
||||
const selectedEnvIndex = getSelectedEnvironmentIndex()
|
||||
if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return
|
||||
|
||||
if (selectedEnvIndex?.type === "MY_ENV") {
|
||||
deleteEnvironment(selectedEnvIndex.index)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
if (selectedEnvIndex?.type === "TEAM_ENV") {
|
||||
pipe(
|
||||
deleteTeamEnvironment(selectedEnvIndex.teamEnvID),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("team_environment.deleted")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
}
|
||||
|
||||
defineActionHandler("modals.environment.new", () => {
|
||||
action.value = "new"
|
||||
showModalDetails.value = true
|
||||
})
|
||||
|
||||
defineActionHandler("modals.environment.delete-selected", () => {
|
||||
showConfirmRemoveEnvModal.value = true
|
||||
})
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
editingVariableName.value = variableName
|
||||
envName === "Global" && editEnvironment("Global")
|
||||
}
|
||||
)
|
||||
@@ -300,7 +251,7 @@ watch(
|
||||
|
||||
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
|
||||
editingVariableName.value = envName
|
||||
if (variableName) editingVariableValue.value = variableName
|
||||
editingVariableValue.value = variableName
|
||||
displayModalNew(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -7,15 +7,22 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
:disabled="editingEnvironmentIndex === 'Global'"
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelEnvEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="editingEnvironmentIndex === 'Global'"
|
||||
@keyup.enter="saveEnvironment"
|
||||
/>
|
||||
<label for="selectLabelEnvEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="variableList" class="p-4">
|
||||
{{ t("environment.variable_list") }}
|
||||
@@ -81,6 +88,7 @@
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -170,7 +178,7 @@ const emit = defineEmits<{
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const editingName = ref<string | null>(null)
|
||||
const name = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
@@ -223,12 +231,10 @@ const liveEnvs = computed(() => {
|
||||
}
|
||||
|
||||
if (props.editingEnvironmentIndex === "Global") {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...vars.value.map((x) => ({ ...x.env, source: name.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
}
|
||||
@@ -238,7 +244,7 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = workingEnv.value?.name ?? null
|
||||
name.value = workingEnv.value?.name ?? null
|
||||
vars.value = pipe(
|
||||
workingEnv.value?.variables ?? [],
|
||||
A.map((e) => ({
|
||||
@@ -271,7 +277,7 @@ const removeEnvironmentVariable = (index: number) => {
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -287,13 +293,13 @@ const saveEnvironment = () => {
|
||||
)
|
||||
|
||||
const environmentUpdated: Environment = {
|
||||
name: editingName.value,
|
||||
name: name.value,
|
||||
variables: filterdVariables,
|
||||
}
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
||||
createEnvironment(name.value, environmentUpdated.variables)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -331,7 +337,7 @@ const saveEnvironment = () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -158,7 +158,5 @@ const duplicateEnvironments = () => {
|
||||
cloneDeep(getGlobalVariables())
|
||||
)
|
||||
} else duplicateEnvironment(props.environmentIndex)
|
||||
|
||||
toast.success(`${t("environment.duplicated")}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -108,7 +109,7 @@ const resetSelectedData = () => {
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
|
||||
@@ -7,15 +7,23 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:input-styles="['floating-input', isViewer && 'opacity-25']"
|
||||
:label="t('action.label')"
|
||||
:disabled="isViewer"
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelEnvEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
:class="isViewer && 'opacity-25'"
|
||||
placeholder=""
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="isViewer"
|
||||
@keyup.enter="saveEnvironment"
|
||||
/>
|
||||
<label for="selectLabelEnvEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="variableList" class="p-4">
|
||||
{{ t("environment.variable_list") }}
|
||||
@@ -86,11 +94,13 @@
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -180,7 +190,7 @@ const emit = defineEmits<{
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const editingName = ref<string | null>(null)
|
||||
const name = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
@@ -206,9 +216,7 @@ const liveEnvs = computed(() => {
|
||||
if (evnExpandError.value) {
|
||||
return []
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -217,7 +225,7 @@ watch(
|
||||
(show) => {
|
||||
if (show) {
|
||||
if (props.action === "new") {
|
||||
editingName.value = null
|
||||
name.value = null
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
@@ -226,7 +234,7 @@ watch(
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
name.value = props.editingEnvironment.environment.name ?? null
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
@@ -264,7 +272,7 @@ const isLoading = ref(false)
|
||||
const saveEnvironment = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
if (!editingName.value) {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -289,7 +297,7 @@ const saveEnvironment = async () => {
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingTeamId,
|
||||
editingName.value
|
||||
name.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
@@ -312,7 +320,7 @@ const saveEnvironment = async () => {
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingEnvironment.id,
|
||||
editingName.value
|
||||
name.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
@@ -331,7 +339,7 @@ const saveEnvironment = async () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("environment.duplicated")}`)
|
||||
toast.success(`${t("team_environment.duplicate")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
class="mb-4"
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
@@ -63,6 +64,7 @@
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -176,7 +178,7 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
|
||||
@@ -9,22 +9,27 @@
|
||||
<template #body>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
:loading="signingInWithGitHub"
|
||||
:icon="IconGithub"
|
||||
:label="`${t('auth.continue_with_github')}`"
|
||||
@click="signInWithGithub"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
:loading="signingInWithGoogle"
|
||||
:icon="IconGoogle"
|
||||
:label="`${t('auth.continue_with_google')}`"
|
||||
@click="signInWithGoogle"
|
||||
/>
|
||||
<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>
|
||||
<form
|
||||
@@ -32,14 +37,24 @@
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
required
|
||||
spellcheck="false"
|
||||
autofocus
|
||||
/>
|
||||
<label for="email">
|
||||
{{ t("auth.email") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
@@ -108,138 +123,124 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, onMounted, ref } from "vue"
|
||||
|
||||
import { useStreamSubscriber } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { platform } from "~/platform"
|
||||
import { setLocalConfig } from "~/newstore/localpersistence"
|
||||
|
||||
import IconGithub from "~icons/auth/github"
|
||||
import IconGoogle from "~icons/auth/google"
|
||||
import IconEmail from "~icons/auth/email"
|
||||
import IconMicrosoft from "~icons/auth/microsoft"
|
||||
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"
|
||||
|
||||
import { LoginItemDef } from "~/platform/auth"
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
setup() {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK
|
||||
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
return {
|
||||
subscribeToStream,
|
||||
t: useI18n(),
|
||||
toast: useToast(),
|
||||
IconGithub,
|
||||
IconGoogle,
|
||||
IconEmail,
|
||||
IconMicrosoft,
|
||||
IconArrowLeft,
|
||||
tosLink,
|
||||
privacyPolicyLink,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
email: "",
|
||||
},
|
||||
signingInWithGoogle: false,
|
||||
signingInWithGitHub: false,
|
||||
signingInWithMicrosoft: false,
|
||||
signingInWithEmail: false,
|
||||
mode: "sign-in",
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
this.subscribeToStream(currentUser$, (user) => {
|
||||
if (user) this.hideModal()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
showLoginSuccess() {
|
||||
this.toast.success(`${this.t("auth.login_success")}`)
|
||||
},
|
||||
async signInWithGoogle() {
|
||||
this.signingInWithGoogle = true
|
||||
|
||||
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 privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
|
||||
|
||||
type AuthProviderItem = {
|
||||
id: string
|
||||
icon: typeof IconGithub
|
||||
label: string
|
||||
action: (...args: any[]) => any
|
||||
isLoading: Ref<boolean>
|
||||
}
|
||||
|
||||
const additonalLoginItems = computed(
|
||||
() => platform.auth.additionalLoginItems ?? []
|
||||
)
|
||||
|
||||
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
|
||||
await item.onClick()
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
subscribeToStream(currentUser$, (user) => {
|
||||
if (user) hideModal()
|
||||
})
|
||||
})
|
||||
|
||||
const showLoginSuccess = () => {
|
||||
toast.success(`${t("auth.login_success")}`)
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
signingInWithGoogle.value = true
|
||||
|
||||
try {
|
||||
await platform.auth.signInUserWithGoogle()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
try {
|
||||
await platform.auth.signInUserWithGoogle()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
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
|
||||
*/
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
signingInWithGoogle.value = false
|
||||
}
|
||||
this.signingInWithGoogle = 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) {
|
||||
this.signingInWithGitHub = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
signingInWithGitHub.value = false
|
||||
return
|
||||
}
|
||||
if (result.type === "success") {
|
||||
// this.showLoginSuccess()
|
||||
} else if (result.type === "account-exists-with-different-cred") {
|
||||
this.toast.info(`${this.t("auth.account_exists")}`, {
|
||||
duration: 0,
|
||||
closeOnSwipe: false,
|
||||
action: {
|
||||
text: `${this.t("action.yes")}`,
|
||||
onClick: async (_, toastObject) => {
|
||||
await result.link()
|
||||
this.showLoginSuccess()
|
||||
|
||||
if (result.type === "success") {
|
||||
// this.showLoginSuccess()
|
||||
} else if (result.type === "account-exists-with-different-cred") {
|
||||
toast.info(`${t("auth.account_exists")}`, {
|
||||
duration: 0,
|
||||
closeOnSwipe: false,
|
||||
action: {
|
||||
text: `${t("action.yes")}`,
|
||||
onClick: async (_, toastObject) => {
|
||||
await result.link()
|
||||
showLoginSuccess()
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log("error logging into github", result.err)
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log("error logging into github", result.err)
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
this.signingInWithGitHub = false
|
||||
},
|
||||
async signInWithMicrosoft() {
|
||||
this.signingInWithMicrosoft = true
|
||||
|
||||
signingInWithGitHub.value = false
|
||||
}
|
||||
|
||||
const signInWithMicrosoft = async () => {
|
||||
signingInWithMicrosoft.value = true
|
||||
|
||||
try {
|
||||
await platform.auth.signInUserWithMicrosoft()
|
||||
// this.showLoginSuccess()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
try {
|
||||
await platform.auth.signInUserWithMicrosoft()
|
||||
// this.showLoginSuccess()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github
|
||||
If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider.
|
||||
The error messages are as follows:
|
||||
@@ -247,84 +248,34 @@ const signInWithMicrosoft = async () => {
|
||||
@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
|
||||
*/
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
signingInWithMicrosoft.value = false
|
||||
}
|
||||
|
||||
const signInWithEmail = async () => {
|
||||
signingInWithEmail.value = true
|
||||
|
||||
await platform.auth
|
||||
.signInWithEmail(form.email)
|
||||
.then(() => {
|
||||
mode.value = "email-sent"
|
||||
setLocalConfig("emailForSignIn", form.email)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
toast.error(e.message)
|
||||
signingInWithEmail.value = false
|
||||
})
|
||||
.finally(() => {
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: "GOOGLE",
|
||||
icon: IconGoogle,
|
||||
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"
|
||||
this.signingInWithMicrosoft = false
|
||||
},
|
||||
async signInWithEmail() {
|
||||
this.signingInWithEmail = true
|
||||
|
||||
await platform.auth
|
||||
.signInWithEmail(this.form.email)
|
||||
.then(() => {
|
||||
this.mode = "email-sent"
|
||||
setLocalConfig("emailForSignIn", this.form.email)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
this.toast.error(e.message)
|
||||
this.signingInWithEmail = false
|
||||
})
|
||||
.finally(() => {
|
||||
this.signingInWithEmail = false
|
||||
})
|
||||
},
|
||||
hideModal() {
|
||||
this.mode = "sign-in"
|
||||
this.toast.clear()
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
||||
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"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
@@ -32,7 +32,7 @@
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'none'
|
||||
authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -43,7 +43,7 @@
|
||||
:active="authName === 'Basic Auth'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'basic'
|
||||
authType = 'basic'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -54,7 +54,7 @@
|
||||
:active="authName === 'Bearer'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'bearer'
|
||||
authType = 'bearer'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -65,7 +65,7 @@
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'oauth-2'
|
||||
authType = 'oauth-2'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -76,7 +76,7 @@
|
||||
:active="authName === 'API key'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'api-key'
|
||||
authType = 'api-key'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -90,8 +90,8 @@
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</HoppSmartCheckbox>-->
|
||||
{{ t("authorization.include_in_url") }}
|
||||
</HoppSmartCheckbox> -->
|
||||
<HoppSmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@@ -115,7 +115,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="auth.authType === 'none'"
|
||||
v-if="authType === 'none'"
|
||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
:text="t('empty.authorization')"
|
||||
@@ -127,47 +127,114 @@
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="auth.authType === 'basic'">
|
||||
<div v-if="authType === 'basic'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.username"
|
||||
v-model="basicUsername"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.password"
|
||||
v-model="basicPassword"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'bearer'">
|
||||
<div v-if="authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.token"
|
||||
v-model="bearerToken"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'oauth-2'">
|
||||
<div v-if="authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.token"
|
||||
v-model="oauth2Token"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization v-model="auth" />
|
||||
<HttpOAuth2Authorization />
|
||||
</div>
|
||||
<div v-if="auth.authType === 'api-key'">
|
||||
<HttpAuthorizationApiKey v-model="auth" />
|
||||
<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>
|
||||
<div
|
||||
@@ -190,45 +257,55 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
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 IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
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 colorMode = useColorMode()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppGQLAuth
|
||||
}>()
|
||||
|
||||
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 authName = computed(() =>
|
||||
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
||||
const auth = useStream(
|
||||
gqlAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setGQLAuth
|
||||
)
|
||||
const authType = pluckRef(auth, "authType")
|
||||
const authName = computed(() => {
|
||||
if (authType.value === "basic") return "Basic Auth"
|
||||
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 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 = () => {
|
||||
auth.value = {
|
||||
@@ -239,4 +316,5 @@ const clearContent = () => {
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const authTippyActions = ref<any | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
<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>
|
||||
@@ -1,235 +0,0 @@
|
||||
<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>
|
||||
@@ -17,136 +17,55 @@
|
||||
<HoppButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:loading="connection.state === 'CONNECTING'"
|
||||
:loading="isLoading"
|
||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||
class="w-32"
|
||||
@click="onConnectClick"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { connection } from "~/helpers/graphql/connection"
|
||||
import { connect } from "~/helpers/graphql/connection"
|
||||
import { disconnect } from "~/helpers/graphql/connection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import {
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
gqlURL$,
|
||||
setGQLURL,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const interceptorService = useService(InterceptorService)
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const connectionSwitchModal = ref(false)
|
||||
|
||||
const connected = computed(() => connection.state === "CONNECTED")
|
||||
|
||||
const url = computed({
|
||||
get: () => currentActiveTab.value?.document.request.url ?? "",
|
||||
set: (value) => {
|
||||
currentActiveTab.value!.document.request.url = value
|
||||
},
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
const auth = useReadonlyStream(gqlAuth$, {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
})
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
gqlConnect()
|
||||
props.conn.connect(url.value, headers.value as any, auth.value)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
} else {
|
||||
disconnect()
|
||||
props.conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
const gqlConnect = () => {
|
||||
connect(url.value, currentActiveTab.value?.document.request.headers)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-schema",
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -2,42 +2,311 @@
|
||||
<div class="flex flex-col flex-1 h-full">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||
:render-inactive-tabs="true"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'query'"
|
||||
:label="`${t('tab.query')}`"
|
||||
:indicator="request.query && request.query.length > 0 ? true : false"
|
||||
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
|
||||
>
|
||||
<GraphqlQuery
|
||||
v-model="request.query"
|
||||
@run-query="runQuery"
|
||||
@save-request="saveRequest"
|
||||
/>
|
||||
<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 gqlRunQuery"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ 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
|
||||
:id="'variables'"
|
||||
:label="`${t('tab.variables')}`"
|
||||
:indicator="
|
||||
request.variables && request.variables.length > 0 ? true : false
|
||||
"
|
||||
:indicator="variableString && variableString.length > 0 ? true : false"
|
||||
>
|
||||
<GraphqlVariable
|
||||
v-model="request.variables"
|
||||
@run-query="runQuery"
|
||||
@save-request="saveRequest"
|
||||
/>
|
||||
<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
|
||||
"
|
||||
/>
|
||||
<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
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
|
||||
>
|
||||
<GraphqlHeaders v-model="request" />
|
||||
<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("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 :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<GraphqlAuthorization v-model="request.auth" />
|
||||
<GraphqlAuthorization />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<CollectionsSaveRequest
|
||||
@@ -49,165 +318,481 @@
|
||||
</template>
|
||||
|
||||
<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 { useToast } from "@composables/toast"
|
||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||
import * as gql from "graphql"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import {
|
||||
GQLResponseEvent,
|
||||
runGQLOperation,
|
||||
gqlMessageEvent,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
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 { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
|
||||
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||
const selectedOptionTab = ref<GQLOptionTabs>("query")
|
||||
const interceptorService = useService(InterceptorService)
|
||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const selectedOptionTab = ref<OptionTabs>("query")
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppGQLRequest
|
||||
response?: GQLResponseEvent[] | null
|
||||
tabId: string
|
||||
}>(),
|
||||
const url = useReadonlyStream(gqlURL$, "")
|
||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
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 }>>([
|
||||
{
|
||||
response: null,
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(["update:modelValue", "update:response"])
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => request.value,
|
||||
(newVal) => {
|
||||
emit("update:modelValue", newVal)
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
},
|
||||
{ deep: 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(
|
||||
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 }
|
||||
)
|
||||
|
||||
const url = computedWithControl(
|
||||
() => currentActiveTab.value,
|
||||
() => currentActiveTab.value.document.request.url
|
||||
)
|
||||
watch(workingHeaders, (newWorkingHeaders) => {
|
||||
const fixedHeaders = pipe(
|
||||
newWorkingHeaders,
|
||||
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(
|
||||
() =>
|
||||
request.value.headers.filter(
|
||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||
).length
|
||||
headers.value.filter((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 runQuery = async (
|
||||
definition: gql.OperationDefinitionNode | null = null
|
||||
) => {
|
||||
|
||||
const copyQuery = () => {
|
||||
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()
|
||||
|
||||
startPageProgress()
|
||||
response.value = "loading"
|
||||
|
||||
try {
|
||||
const runURL = clone(url.value)
|
||||
const runHeaders = clone(request.value.headers)
|
||||
const runQuery = clone(request.value.query)
|
||||
const runVariables = clone(request.value.variables)
|
||||
const runAuth = clone(request.value.auth)
|
||||
const runHeaders = clone(headers.value)
|
||||
const runQuery = clone(gqlQueryString.value)
|
||||
const runVariables = clone(variableString.value)
|
||||
const runAuth = clone(auth.value)
|
||||
|
||||
await runGQLOperation({
|
||||
name: request.value.name,
|
||||
url: runURL,
|
||||
headers: runHeaders,
|
||||
query: runQuery,
|
||||
variables: runVariables,
|
||||
auth: runAuth,
|
||||
operationName: definition?.name?.value,
|
||||
operationType: definition?.operation ?? "query",
|
||||
})
|
||||
const responseText = await props.conn.runQuery(
|
||||
runURL,
|
||||
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,
|
||||
query: runQuery,
|
||||
headers: runHeaders,
|
||||
variables: runVariables,
|
||||
auth: runAuth,
|
||||
}),
|
||||
response: response.value,
|
||||
star: false,
|
||||
})
|
||||
)
|
||||
|
||||
toast.success(`${t("state.finished_in", { duration })}`)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
// response.value = [`${e}`]
|
||||
response.value = `${e}`
|
||||
completePageProgress()
|
||||
|
||||
toast.error(
|
||||
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
|
||||
{}
|
||||
)
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-query",
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
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
|
||||
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 clearGQLQuery = () => {
|
||||
request.value.query = ""
|
||||
|
||||
const saveRequest = () => {
|
||||
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 = () => {
|
||||
gqlQueryString.value = ""
|
||||
}
|
||||
|
||||
const clearGQLVariables = () => {
|
||||
variableString.value = ""
|
||||
}
|
||||
|
||||
defineActionHandler("request.send-cancel", runQuery)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler("request.save-as", () => {
|
||||
showSaveRequestModal.value = true
|
||||
})
|
||||
defineActionHandler("request.reset", clearGQLQuery)
|
||||
|
||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||
selectedOptionTab.value = tab as GQLOptionTabs
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<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>
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
||||
<div v-if="response?.length === 1" class="flex flex-col flex-1">
|
||||
<HoppSmartPlaceholder
|
||||
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
|
||||
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"
|
||||
>
|
||||
@@ -29,18 +37,12 @@
|
||||
'action.copy'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
||||
:icon="copyResponseIcon"
|
||||
@click="copyResponse(response[0].data)"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="schemaEditor" class="flex flex-col flex-1"></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" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -50,34 +52,22 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { reactive, ref } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
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 responseString = useReadonlyStream(gqlResponse$, "")
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
@@ -105,14 +95,14 @@ const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
1000
|
||||
)
|
||||
|
||||
const copyResponse = (str: string) => {
|
||||
copyToClipboard(str)
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const downloadResponse = (str: string) => {
|
||||
const dataToWrite = str
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
@@ -128,14 +118,6 @@ const downloadResponse = (str: string) => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"response.file.download",
|
||||
() => downloadResponse(responseString.value),
|
||||
computed(() => !!props.response && props.response.length > 0)
|
||||
)
|
||||
defineActionHandler(
|
||||
"response.copy",
|
||||
() => copyResponse(responseString.value),
|
||||
computed(() => !!props.response && props.response.length > 0)
|
||||
)
|
||||
defineActionHandler("response.file.download", () => downloadResponse())
|
||||
defineActionHandler("response.copy", () => copyResponse())
|
||||
</script>
|
||||
|
||||