Compare commits
120 Commits
release/20
...
fix/duplic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01200c9e5f | ||
|
|
75193a7aa8 | ||
|
|
b269c239d9 | ||
|
|
72b4a1fc4e | ||
|
|
d2d1674d31 | ||
|
|
a6b57777e3 | ||
|
|
65ef4db86f | ||
|
|
7201147b55 | ||
|
|
dd143c95a9 | ||
|
|
005581ee7d | ||
|
|
1431ecc6d7 | ||
|
|
f34d896095 | ||
|
|
e95ebb9226 | ||
|
|
57365eeae0 | ||
|
|
b22bd97818 | ||
|
|
b953b32ff4 | ||
|
|
0eacd6763b | ||
|
|
8499ac7fec | ||
|
|
4adac4af38 | ||
|
|
fd162e242c | ||
|
|
3e83828722 | ||
|
|
f7dc36e3f1 | ||
|
|
a7566dfd86 | ||
|
|
d4d7a20fbd | ||
|
|
dfb281bcf7 | ||
|
|
c62482e81f | ||
|
|
886847ab7b | ||
|
|
a268cab11e | ||
|
|
e9509b9fa1 | ||
|
|
8db452089c | ||
|
|
a1764023f3 | ||
|
|
b08b63dc73 | ||
|
|
a9a4ebf595 | ||
|
|
a8e279db28 | ||
|
|
d09a3e9237 | ||
|
|
efa40cf6ea | ||
|
|
1a3d9f18ab | ||
|
|
653ccd3240 | ||
|
|
c0806cfd07 | ||
|
|
008eb6b77b | ||
|
|
ac60843183 | ||
|
|
3c3fb1e4a9 | ||
|
|
88212e8cfe | ||
|
|
191fa376d2 | ||
|
|
6efae3a395 | ||
|
|
cb8678f07f | ||
|
|
b32b0f9bcb | ||
|
|
5a91fb53b2 | ||
|
|
b0b6edc58e | ||
|
|
8c57d81718 | ||
|
|
10bb68a538 | ||
|
|
d4d1e27ba9 | ||
|
|
d5c887f311 | ||
|
|
ce7adf6da3 | ||
|
|
c626fb9241 | ||
|
|
f21ed30e10 | ||
|
|
b55970cc7a | ||
|
|
74ad2e43a4 | ||
|
|
2d6282cf8b | ||
|
|
e255c46455 | ||
|
|
15c2c7bb5b | ||
|
|
71bcd22444 | ||
|
|
2d104160f2 | ||
|
|
f7c1825de5 | ||
|
|
2c1fd5d711 | ||
|
|
085fbb2a9b | ||
|
|
05f2d8817b | ||
|
|
81fbb22c51 | ||
|
|
01cf59c663 | ||
|
|
5c8ebaff3e | ||
|
|
0e70c28324 | ||
|
|
88f6a4ae26 | ||
|
|
610538ca02 | ||
|
|
8970ff5c68 | ||
|
|
d1a564d5b8 | ||
|
|
8bb1d19c07 | ||
|
|
c1efa381f0 | ||
|
|
29171d1b6f | ||
|
|
e869d49e16 | ||
|
|
6496bea846 | ||
|
|
39842559b5 | ||
|
|
51efb35aa6 | ||
|
|
9402bb9285 | ||
|
|
5a516f7242 | ||
|
|
3b217d78e7 | ||
|
|
8e153b38dc | ||
|
|
6f38bfb148 | ||
|
|
82b6e08d68 | ||
|
|
31fd6567b7 | ||
|
|
25177bd635 | ||
|
|
6928eb7992 | ||
|
|
8300f9a0a2 | ||
|
|
525ba77739 | ||
|
|
6bc748a267 | ||
|
|
5230d2d3b8 | ||
|
|
c3531c9d8b | ||
|
|
b29c04c28d | ||
|
|
b2af353941 | ||
|
|
9dbce74f5e | ||
|
|
db1cf5cc08 | ||
|
|
09360abf81 | ||
|
|
355bd62b8d | ||
|
|
5650de1183 | ||
|
|
2ee8614b93 | ||
|
|
5632334c9a | ||
|
|
780dd8a713 | ||
|
|
7db3c6d290 | ||
|
|
c765270dfe | ||
|
|
03f667c21d | ||
|
|
f79f3078dc | ||
|
|
6e29a2f6d4 | ||
|
|
6304fd50c3 | ||
|
|
2ec29c47ad | ||
|
|
6f35574d68 | ||
|
|
fc3e3aeaec | ||
|
|
e2b668bee2 | ||
|
|
f112c46bb4 | ||
|
|
331d482b22 | ||
|
|
b07243f131 | ||
|
|
81a7e23a12 |
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
**/*/node_modules
|
||||||
@@ -13,6 +13,7 @@ SESSION_SECRET='add some secret here'
|
|||||||
# Hoppscotch App Domain Config
|
# Hoppscotch App Domain Config
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||||
|
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||||
|
|
||||||
# Google Auth Config
|
# Google Auth Config
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
GOOGLE_CLIENT_ID="************************************************"
|
||||||
|
|||||||
66
.github/workflows/release-push-docker.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: "Push containers to Docker Hub on release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: backend
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: app
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: sh_admin
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: aio
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
|
|||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
diverse, inclusive, and healthy community.
|
diverse, inclusive, and healthy community.
|
||||||
@@ -22,17 +22,17 @@ community include:
|
|||||||
* Giving and gracefully accepting constructive feedback
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
overall community
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
advances of any kind
|
any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
* Publishing others' private information, such as a physical or email address,
|
||||||
address, without their explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
|
|||||||
|
|
||||||
### 2. Warning
|
### 2. Warning
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
**Community Impact**: A violation through a single incident or series of
|
||||||
of actions.
|
actions.
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
interaction with the people involved, including unsolicited interaction with
|
interaction with the people involved, including unsolicited interaction with
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
like social media. Violating these terms may lead to a temporary or
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
permanent ban.
|
ban.
|
||||||
|
|
||||||
### 3. Temporary Ban
|
### 3. Temporary Ban
|
||||||
|
|
||||||
@@ -106,23 +106,27 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
the community.
|
community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
version 2.0, available at
|
version 2.1, available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
Community Impact Guidelines were inspired by
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
https://www.contributor-covenant.org/translations.
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|||||||
190
README.md
@@ -2,23 +2,18 @@
|
|||||||
<a href="https://hoppscotch.io">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<img
|
||||||
src="https://avatars.githubusercontent.com/u/56705483"
|
src="https://avatars.githubusercontent.com/u/56705483"
|
||||||
alt="Hoppscotch Logo"
|
alt="Hoppscotch"
|
||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<h3>
|
||||||
<p>
|
|
||||||
<h3>
|
|
||||||
<b>
|
|
||||||
Hoppscotch
|
|
||||||
</b>
|
|
||||||
</h3>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<b>
|
<b>
|
||||||
Open source API development ecosystem
|
Hoppscotch
|
||||||
</b>
|
</b>
|
||||||
</p>
|
</h3>
|
||||||
|
<b>
|
||||||
|
Open Source API Development Ecosystem
|
||||||
|
</b>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
|
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
|
||||||
@@ -34,23 +29,18 @@
|
|||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<picture>
|
||||||
src="./packages/hoppscotch-common/public/images/banner-light.png"
|
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
alt="Hoppscotch"
|
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
|
||||||
width="100%"
|
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
/>
|
</picture>
|
||||||
</a>
|
|
||||||
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
|
|
||||||
<img
|
|
||||||
src="./packages/hoppscotch-common/public/images/banner-dark.png"
|
|
||||||
alt="Hoppscotch"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
|
||||||
|
|
||||||
#### **Support**
|
#### **Support**
|
||||||
|
|
||||||
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
||||||
@@ -59,9 +49,9 @@
|
|||||||
|
|
||||||
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
||||||
|
|
||||||
⚡️ **Fast:** Send requests and get/copy responses in real-time.
|
⚡️ **Fast:** Send requests and get responses in real time.
|
||||||
|
|
||||||
**HTTP Methods**
|
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
|
||||||
|
|
||||||
- `GET` - Requests retrieve resource information
|
- `GET` - Requests retrieve resource information
|
||||||
- `POST` - The server creates a new entry in a database
|
- `POST` - The server creates a new entry in a database
|
||||||
@@ -74,17 +64,15 @@
|
|||||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
||||||
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||||
|
|
||||||
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||||
|
|
||||||
**Theming**
|
- Choose a theme: System preference, Light, Dark, and Black
|
||||||
|
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
||||||
- Choose a theme: System (default), Light, Dark, and Black
|
|
||||||
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
|
||||||
- Distraction-free Zen mode
|
- Distraction-free Zen mode
|
||||||
|
|
||||||
_Customized themes are synced with cloud / local session_
|
_Customized themes are synced with your cloud/local session._
|
||||||
|
|
||||||
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
|
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
|
||||||
|
|
||||||
- Instant loading with Service Workers
|
- Instant loading with Service Workers
|
||||||
- Offline support
|
- Offline support
|
||||||
@@ -107,7 +95,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
|
|
||||||
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
||||||
|
|
||||||
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
|
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
|
||||||
|
|
||||||
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
||||||
|
|
||||||
@@ -127,7 +115,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- OAuth 2.0
|
- OAuth 2.0
|
||||||
- OIDC Access Token/PKCE
|
- OIDC Access Token/PKCE
|
||||||
|
|
||||||
📢 **Headers:** Describes the format the body of your request is being sent as.
|
📢 **Headers:** Describes the format the body of your request is being sent in.
|
||||||
|
|
||||||
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
||||||
|
|
||||||
@@ -137,14 +125,14 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- FormData, JSON, and many more
|
- FormData, JSON, and many more
|
||||||
- Toggle between key-value and RAW input parameter list
|
- Toggle between key-value and RAW input parameter list
|
||||||
|
|
||||||
👋 **Response:** Contains the status line, headers, and the message/response body.
|
📮 **Response:** Contains the status line, headers, and the message/response body.
|
||||||
|
|
||||||
- Copy response to clipboard
|
- Copy the response to the clipboard
|
||||||
- Download response as a file
|
- Download the response as a file
|
||||||
- View response headers
|
- View response headers
|
||||||
- View raw and preview of HTML, image, JSON, XML responses
|
- View raw and preview HTML, image, JSON, and XML responses
|
||||||
|
|
||||||
⏰ **History:** Request entries are synced with cloud / local session storage to restore with a single click.
|
⏰ **History:** Request entries are synced with your cloud/local session storage.
|
||||||
|
|
||||||
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
||||||
|
|
||||||
@@ -152,7 +140,32 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- Nested folders
|
- Nested folders
|
||||||
- Export and import as a file or GitHub gist
|
- Export and import as a file or GitHub gist
|
||||||
|
|
||||||
_Collections are synced with cloud / local session storage_
|
_Collections are synced with your cloud/local session storage._
|
||||||
|
|
||||||
|
📜 **Pre-Request Scripts:** Snippets of code associated with a request that is executed before the request is sent.
|
||||||
|
|
||||||
|
- Set environment variables
|
||||||
|
- Include timestamp in the request headers
|
||||||
|
- Send a random alphanumeric string in the URL parameters
|
||||||
|
- Any JavaScript functions
|
||||||
|
|
||||||
|
👨👩👧👦 **Teams:** Helps you collaborate across your teams to design, develop, and test APIs faster.
|
||||||
|
|
||||||
|
- Create unlimited teams
|
||||||
|
- Create unlimited shared collections
|
||||||
|
- Create unlimited team members
|
||||||
|
- Role-based access control
|
||||||
|
- Cloud sync
|
||||||
|
- Multiple devices
|
||||||
|
|
||||||
|
👥 **Workspaces:** Organize your personal and team collections environments into workspaces. Easily switch between workspaces to manage multiple projects.
|
||||||
|
|
||||||
|
- Create unlimited workspaces
|
||||||
|
- Switch between personal and team workspaces
|
||||||
|
|
||||||
|
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
||||||
|
|
||||||
|
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
||||||
|
|
||||||
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
||||||
|
|
||||||
@@ -161,60 +174,31 @@ _Collections are synced with cloud / local session storage_
|
|||||||
- Access APIs served in non-HTTPS (`http://`) endpoints
|
- Access APIs served in non-HTTPS (`http://`) endpoints
|
||||||
- Use your Proxy URL
|
- Use your Proxy URL
|
||||||
|
|
||||||
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**_
|
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**._
|
||||||
|
|
||||||
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
|
|
||||||
|
|
||||||
- Set environment variables
|
|
||||||
- Include timestamp in the request headers
|
|
||||||
- Send a random alphanumeric string in the URL parameters
|
|
||||||
- Any JavaScript functions
|
|
||||||
|
|
||||||
📄 **API Documentation:** Create and share dynamic API documentation easily, quickly.
|
|
||||||
|
|
||||||
1. Add your requests to Collections and Folders
|
|
||||||
2. Export Collections and easily share your APIs with the rest of your team
|
|
||||||
3. Import Collections and Generate Documentation on-the-go
|
|
||||||
|
|
||||||
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
|
||||||
|
|
||||||
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
|
||||||
|
|
||||||
🌎 **i18n:** Experience the app in your language.
|
🌎 **i18n:** Experience the app in your language.
|
||||||
|
|
||||||
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
|
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md) and the process for submitting pull requests to us.
|
||||||
|
|
||||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
|
||||||
|
|
||||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
|
**Sign in with:**
|
||||||
- **[CLI β](https://github.com/hoppscotch/hopp-cli)** - A CLI solution for Hoppscotch
|
|
||||||
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that simplifies access to Hoppscotch
|
|
||||||
|
|
||||||
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
|
||||||
|
|
||||||
> **Extensions fixes `CORS` issues.**
|
|
||||||
|
|
||||||
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
|
|
||||||
|
|
||||||
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
|
||||||
|
|
||||||
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
|
|
||||||
|
|
||||||
**Sign in with**
|
|
||||||
|
|
||||||
- GitHub
|
- GitHub
|
||||||
- Google
|
- Google
|
||||||
- Microsoft
|
- Microsoft
|
||||||
- Email
|
- Email
|
||||||
|
- SSO (Single Sign-On)[^EE]
|
||||||
|
|
||||||
**Synchronize your data**
|
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
|
||||||
|
|
||||||
|
- Workspaces
|
||||||
- History
|
- History
|
||||||
- Collections
|
- Collections
|
||||||
- Environments
|
- Environments
|
||||||
- Settings
|
- Settings
|
||||||
|
|
||||||
✅ **Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
|
✅ **Post-Request Tests:** Write tests associated with a request that is executed after the request's response.
|
||||||
|
|
||||||
- Check the status code as an integer
|
- Check the status code as an integer
|
||||||
- Filter response headers
|
- Filter response headers
|
||||||
@@ -222,7 +206,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
- Set environment variables
|
- Set environment variables
|
||||||
- Write JavaScript code
|
- Write JavaScript code
|
||||||
|
|
||||||
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
|
🌱 **Environments:** Environment variables allow you to store and reuse values in your requests and scripts.
|
||||||
|
|
||||||
- Unlimited environments and variables
|
- Unlimited environments and variables
|
||||||
- Initialize through the pre-request script
|
- Initialize through the pre-request script
|
||||||
@@ -241,22 +225,31 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
👨👩👧👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
|
|
||||||
|
|
||||||
- Unlimited teams
|
|
||||||
- Unlimited shared collections
|
|
||||||
- Unlimited team members
|
|
||||||
- Role-based access control
|
|
||||||
- Cloud sync
|
|
||||||
- Multiple devices
|
|
||||||
|
|
||||||
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||||
|
|
||||||
- Entries are separated by newline
|
- Entries are separated by newline
|
||||||
- Keys and values are separated by `:`
|
- Keys and values are separated by `:`
|
||||||
- Prepend `#` to any row you want to add but keep disabled
|
- Prepend `#` to any row you want to add but keep disabled
|
||||||
|
|
||||||
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
|
🎛️ **Admin dashboard:** Manage your team and invite members.
|
||||||
|
|
||||||
|
- Insights
|
||||||
|
- Manage users
|
||||||
|
- Manage teams
|
||||||
|
|
||||||
|
📦 **Add-ons:** Official add-ons for hoppscotch.
|
||||||
|
|
||||||
|
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
|
||||||
|
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
|
||||||
|
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
|
||||||
|
|
||||||
|
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
||||||
|
|
||||||
|
> **Extensions fix `CORS` issues.**
|
||||||
|
|
||||||
|
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
||||||
|
|
||||||
|
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
|
||||||
|
|
||||||
## **Demo**
|
## **Demo**
|
||||||
|
|
||||||
@@ -268,18 +261,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
2. Click "Send" to simulate the request
|
2. Click "Send" to simulate the request
|
||||||
3. View the response
|
3. View the response
|
||||||
|
|
||||||
## **Built with**
|
|
||||||
|
|
||||||
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
|
|
||||||
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Windi CSS](https://windicss.org)
|
|
||||||
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
||||||
- [TypeScript](https://www.typescriptlang.org)
|
|
||||||
- [Vue](https://vuejs.org)
|
|
||||||
- [Vite](https://vitejs.dev)
|
|
||||||
|
|
||||||
## **Developing**
|
## **Developing**
|
||||||
|
|
||||||
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
||||||
|
|
||||||
## **Contributing**
|
## **Contributing**
|
||||||
|
|
||||||
@@ -297,7 +281,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
|
|||||||
|
|
||||||
## **Authors**
|
## **Authors**
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
|
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
||||||
@@ -309,4 +293,6 @@ This project exists thanks to all the people who contribute — [contribute](CON
|
|||||||
|
|
||||||
## **License**
|
## **License**
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.
|
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) — see the [`LICENSE`](LICENSE) file for details.
|
||||||
|
|
||||||
|
[^EE]: Enterprise edition feature. [Learn more](https://docs.hoppscotch.io/documentation/self-host/getting-started).
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
This document outlines security procedures and general policies for the Hoppscotch project.
|
This document outlines security procedures and general policies for the Hoppscotch project.
|
||||||
|
|
||||||
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
- [Security Policy](#security-policy)
|
||||||
3. [Incident response process](#incident-response-process)
|
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
||||||
|
- [Incident response process](#incident-response-process)
|
||||||
|
|
||||||
## Reporting a security vulnerability
|
## Reporting a security vulnerability
|
||||||
|
|
||||||
|
|||||||
@@ -9,26 +9,24 @@ Before you start working on a new language, please look through the [open pull r
|
|||||||
if there is no existing translation, you can create a new one by following these steps:
|
if there is no existing translation, you can create a new one by following these steps:
|
||||||
|
|
||||||
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||||
2. **Checkout the `i18n` branch for latest translations.**
|
2. **Checkout the `main` branch for latest translations.**
|
||||||
3. **Create a new branch for your translation with base branch `i18n`.**
|
3. **Create a new branch for your translation with base branch `main`.**
|
||||||
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
||||||
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
||||||
6. **Translate the strings in the target language file.**
|
6. **Translate the strings in the target language file.**
|
||||||
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
|
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
|
||||||
8. **Save & commit changes.**
|
8. **Save and commit changes.**
|
||||||
9. **Send a pull request.**
|
9. **Send a pull request.**
|
||||||
|
|
||||||
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
|
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
|
||||||
|
|
||||||
`i18n` branch will be merged into `main` branch once every week.
|
|
||||||
|
|
||||||
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
|
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
|
||||||
|
|
||||||
## Updating a translation
|
## Updating a translation
|
||||||
|
|
||||||
### Corrections
|
### Corrections
|
||||||
|
|
||||||
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, open an issue.
|
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, [open an issue](https://github.com/hoppscotch/hoppscotch/issues/new/choose).
|
||||||
|
|
||||||
### Broken links
|
### Broken links
|
||||||
|
|
||||||
|
|||||||
11
aio.Caddyfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
:3000 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/selfhost-web
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
:3100 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/sh-admin
|
||||||
|
file_server
|
||||||
|
}
|
||||||
72
aio_run.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/local/bin/node
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { execSync, spawn } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
|
import process from "process"
|
||||||
|
|
||||||
|
function runChildProcessWithPrefix(command, args, prefix) {
|
||||||
|
const childProcess = spawn(command, args);
|
||||||
|
|
||||||
|
childProcess.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString().trim().split('\n');
|
||||||
|
output.forEach((line) => {
|
||||||
|
console.log(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.stderr.on('data', (data) => {
|
||||||
|
const error = data.toString().trim().split('\n');
|
||||||
|
error.forEach((line) => {
|
||||||
|
console.error(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
console.log(`${prefix} Child process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('error', (stuff) => {
|
||||||
|
console.log("error")
|
||||||
|
console.log(stuff)
|
||||||
|
})
|
||||||
|
|
||||||
|
return childProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFileContent = Object.entries(process.env)
|
||||||
|
.filter(([env]) => env.startsWith("VITE_"))
|
||||||
|
.map(([env, val]) => `${env}=${
|
||||||
|
(val.startsWith("\"") && val.endsWith("\""))
|
||||||
|
? val
|
||||||
|
: `"${val}"`
|
||||||
|
}`)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
fs.writeFileSync("build.env", envFileContent)
|
||||||
|
|
||||||
|
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
||||||
|
|
||||||
|
fs.rmSync("build.env")
|
||||||
|
|
||||||
|
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
||||||
|
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
||||||
|
|
||||||
|
caddyProcess.on("exit", (code) => {
|
||||||
|
console.log(`Exiting process because Caddy Server exited with code ${code}`)
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
backendProcess.on("exit", (code) => {
|
||||||
|
console.log(`Exiting process because Backend Server exited with code ${code}`)
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log("SIGINT received, exiting...")
|
||||||
|
|
||||||
|
caddyProcess.kill("SIGINT")
|
||||||
|
backendProcess.kill("SIGINT")
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
@@ -8,23 +8,25 @@ services:
|
|||||||
hoppscotch-backend:
|
hoppscotch-backend:
|
||||||
container_name: hoppscotch-backend
|
container_name: hoppscotch-backend
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: prod
|
target: backend
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
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)
|
# 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
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3000
|
- PORT=3170
|
||||||
volumes:
|
volumes:
|
||||||
- ./packages/hoppscotch-backend/:/usr/src/app
|
# 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/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-db
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3170:3000"
|
- "3170:3170"
|
||||||
|
|
||||||
# The main hoppscotch app. This will be hosted at port 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
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
@@ -32,8 +34,9 @@ services:
|
|||||||
hoppscotch-app:
|
hoppscotch-app:
|
||||||
container_name: hoppscotch-app
|
container_name: hoppscotch-app
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
target: app
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -47,8 +50,9 @@ services:
|
|||||||
hoppscotch-sh-admin:
|
hoppscotch-sh-admin:
|
||||||
container_name: hoppscotch-sh-admin
|
container_name: hoppscotch-sh-admin
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
target: sh_admin
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -56,16 +60,91 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3100:8080"
|
- "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
|
# The preset DB service, you can delete/comment the below lines if
|
||||||
# you are using an external postgres instance
|
# you are using an external postgres instance
|
||||||
# This will be exposed at port 5432
|
# This will be exposed at port 5432
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
image: postgres
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
user: postgres
|
||||||
environment:
|
environment:
|
||||||
|
# The default user defined by the docker image
|
||||||
|
POSTGRES_USER: postgres
|
||||||
# NOTE: Please UPDATE THIS PASSWORD!
|
# NOTE: Please UPDATE THIS PASSWORD!
|
||||||
POSTGRES_PASSWORD: testpass
|
POSTGRES_PASSWORD: testpass
|
||||||
POSTGRES_DB: hoppscotch
|
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"
|
||||||
|
|||||||
14
healthcheck.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
curlCheck() {
|
||||||
|
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
|
||||||
|
echo "URL request failed!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "URL request succeeded!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
curlCheck "http://localhost:3000"
|
||||||
|
curlCheck "http://localhost:3100"
|
||||||
|
curlCheck "http://localhost:3170/ping"
|
||||||
@@ -32,5 +32,14 @@
|
|||||||
"@types/node": "^17.0.24",
|
"@types/node": "^17.0.24",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"http-server": "^14.1.1"
|
"http-server": "^14.1.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"packageExtensions": {
|
||||||
|
"httpsnippet@^3.0.1": {
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "6.12.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.2.0",
|
"@codemirror/language": "^6.9.0",
|
||||||
"@lezer/highlight": "^1.0.0",
|
"@lezer/highlight": "^1.1.6",
|
||||||
"@lezer/lr": "^1.2.0"
|
"@lezer/lr": "^1.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.1.0",
|
"@lezer/generator": "^1.5.0",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"rollup": "^2.70.2",
|
"rollup": "^2.70.2",
|
||||||
"rollup-plugin-dts": "^4.2.1",
|
"rollup-plugin-dts": "^4.2.1",
|
||||||
|
|||||||
24
packages/dioc/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
141
packages/dioc/README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# dioc
|
||||||
|
|
||||||
|
A small and lightweight dependency injection / inversion of control system.
|
||||||
|
|
||||||
|
### About
|
||||||
|
|
||||||
|
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Service, Container } from "dioc"
|
||||||
|
|
||||||
|
// Here is a simple service, which you can define by extending the Service class
|
||||||
|
// and providing an ID static field (of type string)
|
||||||
|
export class PersistenceService extends Service {
|
||||||
|
// This should be unique for each container
|
||||||
|
public static ID = "PERSISTENCE_SERVICE"
|
||||||
|
|
||||||
|
public read(key: string): string | undefined {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
public write(key: string, value: string) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TodoServiceEvent =
|
||||||
|
| { type: "TODO_CREATED"; index: number }
|
||||||
|
| { type: "TODO_DELETED"; index: number }
|
||||||
|
|
||||||
|
// Services have a built in event system
|
||||||
|
// Define the generic argument to say what are the possible emitted values
|
||||||
|
export class TodoService extends Service<TodoServiceEvent> {
|
||||||
|
public static ID = "TODO_SERVICE"
|
||||||
|
|
||||||
|
// Inject persistence service into this service
|
||||||
|
private readonly persistence = this.bind(PersistenceService)
|
||||||
|
|
||||||
|
public todos = []
|
||||||
|
|
||||||
|
// Service constructors cannot have arguments
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTodo(text: string) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// You can access services via the bound fields
|
||||||
|
this.persistence.write("todos", JSON.stringify(this.todos))
|
||||||
|
|
||||||
|
// This is how you emit an event
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_CREATED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeTodo(index: number) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_DELETED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services need a container to run in
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
// You can initialize and get services using Container#bind
|
||||||
|
// It will automatically initialize the service (and its dependencies)
|
||||||
|
const todoService = container.bind(TodoService) // Returns an instance of TodoService
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Unit Test)
|
||||||
|
|
||||||
|
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
|
||||||
|
import { describe, it, expect, vi } from "vitest"
|
||||||
|
|
||||||
|
describe("TodoService", () => {
|
||||||
|
it("addTodo writes to persistence", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const writeFn = vi.fn()
|
||||||
|
|
||||||
|
// The first parameter is the service to mock and the second parameter
|
||||||
|
// is the mocked service fields and functions
|
||||||
|
container.bindMock(PersistenceService, {
|
||||||
|
read: () => undefined, // Not really important for this test
|
||||||
|
write: writeFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// the peristence service bind in TodoService will now use the
|
||||||
|
// above defined mocked implementation
|
||||||
|
const todoService = container.bind(TodoService)
|
||||||
|
|
||||||
|
todoService.addTodo("sup")
|
||||||
|
|
||||||
|
expect(writeFn).toHaveBeenCalledOnce()
|
||||||
|
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Vue)
|
||||||
|
|
||||||
|
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
|
||||||
|
|
||||||
|
In the app entry point:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "vue"
|
||||||
|
import { diocPlugin } from "dioc/vue"
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
|
||||||
|
app.use(diocPlugin, {
|
||||||
|
container: new Container(), // You can pass in the container you want to provide to the components here
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In your Vue components:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { TodoService } from "./demo.ts" // The above demo
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
|
||||||
|
const todoService = useService(TodoService) // Returns an instance of the TodoService class
|
||||||
|
</script>
|
||||||
|
```
|
||||||
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/main.d.ts"
|
||||||
|
export * from "./dist/main.d.ts"
|
||||||
147
packages/dioc/lib/container.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Service } from "./service"
|
||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current container instance in the current operating context.
|
||||||
|
*
|
||||||
|
* NOTE: This should not be used outside of dioc library code
|
||||||
|
*/
|
||||||
|
export let currentContainer: Container | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events emitted by the container
|
||||||
|
*
|
||||||
|
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
|
||||||
|
* `SERVICE_INIT` - emitted when a service is initialized
|
||||||
|
*/
|
||||||
|
export type ContainerEvent =
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_BIND';
|
||||||
|
|
||||||
|
/** The Service ID of the service being bounded (the dependency) */
|
||||||
|
boundeeID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Service ID of the bounder that is binding the boundee (the dependent)
|
||||||
|
*
|
||||||
|
* NOTE: This will be undefined if the service is bound directly to the container
|
||||||
|
*/
|
||||||
|
bounderID: string | undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_INIT';
|
||||||
|
|
||||||
|
/** The Service ID of the service being initialized */
|
||||||
|
serviceID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
|
||||||
|
*/
|
||||||
|
export class Container {
|
||||||
|
/** Used during the `bind` operation to detect circular dependencies */
|
||||||
|
private bindStack: string[] = []
|
||||||
|
|
||||||
|
/** The map of bound services to their IDs */
|
||||||
|
protected boundMap = new Map<string, Service<unknown>>()
|
||||||
|
|
||||||
|
/** The RxJS observable representing the event stream */
|
||||||
|
protected event$ = new Subject<ContainerEvent>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a container has the given service bound
|
||||||
|
* @param service The service to check for
|
||||||
|
*/
|
||||||
|
public hasBound<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): boolean {
|
||||||
|
return this.boundMap.has(service.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the service bound to the container with the given ID or if not found, undefined.
|
||||||
|
*
|
||||||
|
* NOTE: This is an advanced method and should not be used as much as possible.
|
||||||
|
*
|
||||||
|
* @param serviceID The ID of the service to get
|
||||||
|
*/
|
||||||
|
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
|
||||||
|
return this.boundMap.get(serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a service to the container. This is equivalent to marking a service as a dependency.
|
||||||
|
* @param service The class reference of a service to bind
|
||||||
|
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
|
||||||
|
*/
|
||||||
|
public bind<T extends typeof Service<any> & { ID: string }>(
|
||||||
|
service: T,
|
||||||
|
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
|
||||||
|
): InstanceType<T> {
|
||||||
|
// We need to store the current container in a variable so that we can restore it after the bind operation
|
||||||
|
const oldCurrentContainer = currentContainer;
|
||||||
|
currentContainer = this;
|
||||||
|
|
||||||
|
// If the service is already bound, return the existing instance
|
||||||
|
if (this.hasBound(service)) {
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect circular dependency and throw error
|
||||||
|
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
|
||||||
|
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
|
||||||
|
|
||||||
|
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the service ID onto the bind stack to detect circular dependencies
|
||||||
|
this.bindStack.push(service.ID)
|
||||||
|
|
||||||
|
// Initialize the service and emit events
|
||||||
|
|
||||||
|
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
|
||||||
|
const instance: Service<any> = new (service as any)()
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, instance)
|
||||||
|
|
||||||
|
this.bindStack.pop()
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_INIT',
|
||||||
|
serviceID: service.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Restore the current container
|
||||||
|
currentContainer = oldCurrentContainer;
|
||||||
|
|
||||||
|
// We expect the return type to match the service definition
|
||||||
|
return instance as InstanceType<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator of the currently bound service IDs and their instances
|
||||||
|
*/
|
||||||
|
public getBoundServices(): IterableIterator<[string, Service<any>]> {
|
||||||
|
return this.boundMap.entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the public container event stream
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<ContainerEvent> {
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./container"
|
||||||
|
export * from "./service"
|
||||||
65
packages/dioc/lib/service.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
import { Container, currentContainer } from './container'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Dioc service that can bound to a container and can bind dependency services.
|
||||||
|
*
|
||||||
|
* NOTE: Services cannot have a constructor that takes arguments.
|
||||||
|
*
|
||||||
|
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
|
||||||
|
*/
|
||||||
|
export abstract class Service<EventDef = {}> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal event stream of the service
|
||||||
|
*/
|
||||||
|
private event$ = new Subject<EventDef>()
|
||||||
|
|
||||||
|
/** The container the service is bound to */
|
||||||
|
#container: Container
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#container = currentContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a dependency service into this service.
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error('No currentContainer defined.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the container the service is bound to
|
||||||
|
*/
|
||||||
|
protected getContainer(): Container {
|
||||||
|
return this.#container
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event on the service's event stream
|
||||||
|
* @param event The event to emit
|
||||||
|
*/
|
||||||
|
protected emit(event: EventDef) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the event stream of the service
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<EventDef> {
|
||||||
|
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/dioc/lib/testing.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Container, Service } from "./main";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container that can be used for writing tests, contains additional methods
|
||||||
|
* for binding suitable for writing tests. (see `bindMock`).
|
||||||
|
*/
|
||||||
|
export class TestContainer extends Container {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a mock service to the container.
|
||||||
|
*
|
||||||
|
* @param service
|
||||||
|
* @param mock
|
||||||
|
*/
|
||||||
|
public bindMock<
|
||||||
|
T extends typeof Service<any> & { ID: string },
|
||||||
|
U extends Partial<InstanceType<T>>
|
||||||
|
>(service: T, mock: U): U {
|
||||||
|
if (this.boundMap.has(service.ID)) {
|
||||||
|
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, mock as any)
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/dioc/lib/vue.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Plugin, inject } from "vue"
|
||||||
|
import { Container } from "./container"
|
||||||
|
import { Service } from "./service"
|
||||||
|
|
||||||
|
const VUE_CONTAINER_KEY = Symbol()
|
||||||
|
|
||||||
|
// TODO: Some Vue version issue with plugin generics is breaking type checking
|
||||||
|
/**
|
||||||
|
* The Vue Dioc Plugin, this allows the composables to work and access the container
|
||||||
|
*
|
||||||
|
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
|
||||||
|
*/
|
||||||
|
export const diocPlugin: Plugin = {
|
||||||
|
install(app, { container }) {
|
||||||
|
app.provide(VUE_CONTAINER_KEY, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that binds a service to a Vue Component
|
||||||
|
*
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
export function useService<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): InstanceType<T> {
|
||||||
|
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
throw new Error("Container not found, did you forget to install the dioc plugin?")
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.bind(service)
|
||||||
|
}
|
||||||
54
packages/dioc/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "dioc",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"main": "./dist/counter.umd.cjs",
|
||||||
|
"module": "./dist/counter.js",
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./vue": {
|
||||||
|
"types": "./dist/vue.d.ts",
|
||||||
|
"require": "./dist/vue.cjs",
|
||||||
|
"import": "./dist/vue.js"
|
||||||
|
},
|
||||||
|
"./testing": {
|
||||||
|
"types": "./dist/testing.d.ts",
|
||||||
|
"require": "./dist/testing.cjs",
|
||||||
|
"import": "./dist/testing.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build && tsc --emitDeclarationOnly",
|
||||||
|
"prepare": "pnpm run build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"do-test": "pnpm run test",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vite": "^4.0.4",
|
||||||
|
"vitest": "^0.29.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
262
packages/dioc/test/container.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { it, expect, describe, vi } from "vitest"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { Container, currentContainer, ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Container", () => {
|
||||||
|
describe("getBoundServiceWithID", () => {
|
||||||
|
it("returns the service instance if it is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined if the service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the service to it", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(service.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("after bind, the current container is set back to its previous value", () => {
|
||||||
|
const originalValue = currentContainer
|
||||||
|
|
||||||
|
const container = new Container()
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(currentContainer).toBe(originalValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("dependent services are registered in the same container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(serviceB.serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceA2 = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceA).toBe(serviceA2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an initialized service as a dependency returns the same instance", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const instance = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
// We only care about the bind event of TestServiceA
|
||||||
|
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasBound", () => {
|
||||||
|
it("returns true if the given service is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if the given service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true when the service is bound because it is a dependency of another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns an observable which emits events correctly when services are initialized", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an observable which emits events correctly when services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceB.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getBoundServices", () => {
|
||||||
|
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const instanceB = container.bind(TestServiceB)
|
||||||
|
const instanceA = instanceB.serviceA
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([
|
||||||
|
[TestServiceA.ID, instanceA],
|
||||||
|
[TestServiceB.ID, instanceB],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an empty iterator if no services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
66
packages/dioc/test/service.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { Service, Container } from "../lib/main"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service<"test"> {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public emitTestEvent() {
|
||||||
|
this.emit("test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Service", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("throws an error if the service is initialized without a container", () => {
|
||||||
|
expect(() => new TestServiceA()).toThrowError(
|
||||||
|
"Tried to initialize service with no container (ID: TestServiceA)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the dependency service using the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getContainer", () => {
|
||||||
|
it("returns the container the service is bound to", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
|
||||||
|
expect(serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns the valid event stream of the service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn()
|
||||||
|
|
||||||
|
serviceB.getEventStream().subscribe(serviceFunc)
|
||||||
|
|
||||||
|
serviceB.emitTestEvent()
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith("test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
92
packages/dioc/test/test-container.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { TestContainer } from "../lib/testing"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return "real"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// declared public to help with testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return this.serviceA.test()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TestContainer", () => {
|
||||||
|
describe("bindMock", () => {
|
||||||
|
it("returns the fake service defined", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeService = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = container.bindMock(TestServiceA, fakeService)
|
||||||
|
|
||||||
|
expect(result).toBe(fakeService)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("new services bound to the container get the mock service", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(fakeServiceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits SERVICE_BIND event", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<[ContainerEvent, void]>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
serviceFunc(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if service already bound", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
}).toThrowError(
|
||||||
|
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/testing.d.ts"
|
||||||
|
export * from "./dist/testing.d.ts"
|
||||||
21
packages/dioc/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["lib"]
|
||||||
|
}
|
||||||
16
packages/dioc/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: {
|
||||||
|
index: './lib/main.ts',
|
||||||
|
vue: './lib/vue.ts',
|
||||||
|
testing: './lib/testing.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/vue.d.ts"
|
||||||
|
export * from "./dist/vue.d.ts"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.7",
|
"version": "2023.8.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.0",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/throttler": "^4.0.0",
|
"@nestjs/throttler": "^4.0.0",
|
||||||
"@prisma/client": "^4.7.1",
|
"@prisma/client": "^4.16.2",
|
||||||
"apollo-server-express": "^3.11.1",
|
"apollo-server-express": "^3.11.1",
|
||||||
"apollo-server-plugin-base": "^3.7.1",
|
"apollo-server-plugin-base": "^3.7.1",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
"prisma": "^4.7.1",
|
"prisma": "^4.16.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.6.0"
|
"rxjs": "^7.6.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ datasource db {
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
|
|||||||
@@ -411,6 +411,23 @@ export class AdminResolver {
|
|||||||
return deletedTeam.right;
|
return deletedTeam.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Revoke a team Invite by Invite ID',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async revokeTeamInviteByAdmin(
|
||||||
|
@Args({
|
||||||
|
name: 'inviteID',
|
||||||
|
description: 'Team Invite ID',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
inviteID: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const invite = await this.adminService.revokeTeamInviteByID(inviteID);
|
||||||
|
if (E.isLeft(invite)) throwErr(invite.left);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
@Subscription(() => InvitedUser, {
|
@Subscription(() => InvitedUser, {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
@@ -181,7 +182,7 @@ export class AdminService {
|
|||||||
* @returns an array team invitations
|
* @returns an array team invitations
|
||||||
*/
|
*/
|
||||||
async pendingInvitationCountInTeam(teamID: string) {
|
async pendingInvitationCountInTeam(teamID: string) {
|
||||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
const invitations = await this.teamInvitationService.getTeamInvitations(
|
||||||
teamID,
|
teamID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -257,7 +258,7 @@ export class AdminService {
|
|||||||
if (E.isRight(userInvitation)) {
|
if (E.isRight(userInvitation)) {
|
||||||
await this.teamInvitationService.revokeInvitation(
|
await this.teamInvitationService.revokeInvitation(
|
||||||
userInvitation.right.id,
|
userInvitation.right.id,
|
||||||
)();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return E.right(addedUser.right);
|
return E.right(addedUser.right);
|
||||||
@@ -416,4 +417,19 @@ export class AdminService {
|
|||||||
if (E.isLeft(team)) return E.left(team.left);
|
if (E.isLeft(team)) return E.left(team.left);
|
||||||
return E.right(team.right);
|
return E.right(team.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a team invite by ID
|
||||||
|
* @param inviteID Team Invite ID
|
||||||
|
* @returns an Either of boolean or error
|
||||||
|
*/
|
||||||
|
async revokeTeamInviteByID(inviteID: string) {
|
||||||
|
const teamInvite = await this.teamInvitationService.revokeInvitation(
|
||||||
|
inviteID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
|
||||||
|
|
||||||
|
return E.right(teamInvite.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
packages/hoppscotch-backend/src/app.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('ping')
|
||||||
|
export class AppController {
|
||||||
|
@Get()
|
||||||
|
ping(): string {
|
||||||
|
return 'Success';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
|
|||||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -81,5 +82,6 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
|||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
|
controllers: [AppController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
|
||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -19,12 +19,18 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import { authCookieHandler, throwHTTPErr } from './helper';
|
import {
|
||||||
|
AuthProvider,
|
||||||
|
authCookieHandler,
|
||||||
|
authProviderCheck,
|
||||||
|
throwHTTPErr,
|
||||||
|
} from './helper';
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -39,6 +45,9 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
|
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
|
|||||||
import { GoogleStrategy } from './strategies/google.strategy';
|
import { GoogleStrategy } from './strategies/google.strategy';
|
||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -26,9 +27,9 @@ import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
|||||||
AuthService,
|
AuthService,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RTJwtStrategy,
|
RTJwtStrategy,
|
||||||
GoogleStrategy,
|
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||||
GithubStrategy,
|
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||||
MicrosoftStrategy,
|
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export class AuthService {
|
|||||||
url = process.env.VITE_BASE_URL;
|
url = process.env.VITE_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendAuthEmail(email, {
|
await this.mailerService.sendEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard extends AuthGuard('microsoft') {
|
export class MicrosoftSSOGuard
|
||||||
|
extends AuthGuard('microsoft')
|
||||||
|
implements CanActivate
|
||||||
|
{
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||||
|
throwHTTPErr({
|
||||||
|
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { COOKIES_NOT_FOUND } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -16,6 +17,13 @@ export enum Origin {
|
|||||||
APP = 'app',
|
APP = 'app',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AuthProvider {
|
||||||
|
GOOGLE = 'GOOGLE',
|
||||||
|
GITHUB = 'GITHUB',
|
||||||
|
MICROSOFT = 'MICROSOFT',
|
||||||
|
EMAIL = 'EMAIL',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function allows throw to be used as an expression
|
* This function allows throw to be used as an expression
|
||||||
* @param errMessage Message present in the error message
|
* @param errMessage Message present in the error message
|
||||||
@@ -97,3 +105,25 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
|||||||
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
|
||||||
|
*
|
||||||
|
* @param provider Provider we want to check the presence of
|
||||||
|
* @returns Boolean if provider specified is present or not
|
||||||
|
*/
|
||||||
|
export function authProviderCheck(provider: string) {
|
||||||
|
if (!provider) {
|
||||||
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
|
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||||
|
provider.trim().toUpperCase(),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!envVariables.includes(provider.toUpperCase())) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,30 @@ export const AUTH_FAIL = 'auth/fail';
|
|||||||
*/
|
*/
|
||||||
export const JSON_INVALID = 'json_invalid';
|
export const JSON_INVALID = 'json_invalid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Provider not specified
|
||||||
|
* (Auth)
|
||||||
|
*/
|
||||||
|
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_EMPTY_AUTH_PROVIDERS =
|
||||||
|
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to delete a user data document from fb firestore but failed.
|
* Tried to delete a user data document from fb firestore but failed.
|
||||||
* (FirebaseService)
|
* (FirebaseService)
|
||||||
@@ -312,6 +336,13 @@ export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
|||||||
*/
|
*/
|
||||||
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalid TEAM ENVIRONMENT name
|
||||||
|
* (TeamEnvironmentsService)
|
||||||
|
*/
|
||||||
|
export const TEAM_ENVIRONMENT_SHORT_NAME =
|
||||||
|
'team_environment/short_name' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user is not a member of the team of the given environment
|
* The user is not a member of the team of the given environment
|
||||||
* (GqlTeamEnvTeamGuard)
|
* (GqlTeamEnvTeamGuard)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
UserMagicLinkMailDescription,
|
UserMagicLinkMailDescription,
|
||||||
} from './MailDescriptions';
|
} from './MailDescriptions';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { EMAIL_FAILED } from 'src/errors';
|
import { EMAIL_FAILED } from 'src/errors';
|
||||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||||
|
|
||||||
@@ -35,33 +34,14 @@ export class MailerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an email to the given email address given a mail description
|
* Sends an email to the given email address given a mail description
|
||||||
* @param to The email address to be sent to (NOTE: this is not validated)
|
* @param to Receiver's email id
|
||||||
* @param mailDesc Definition of what email to be sent
|
* @param mailDesc Definition of what email to be sent
|
||||||
|
* @returns Response if email was send successfully or not
|
||||||
*/
|
*/
|
||||||
sendMail(
|
async sendEmail(
|
||||||
to: string,
|
to: string,
|
||||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||||
) {
|
) {
|
||||||
return TE.tryCatch(
|
|
||||||
async () => {
|
|
||||||
await this.nestMailerService.sendMail({
|
|
||||||
to,
|
|
||||||
template: mailDesc.template,
|
|
||||||
subject: this.resolveSubjectForMailDesc(mailDesc),
|
|
||||||
context: mailDesc.variables,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => EMAIL_FAILED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param to Receiver's email id
|
|
||||||
* @param mailDesc Details of email to be sent for Magic-Link auth
|
|
||||||
* @returns Response if email was send successfully or not
|
|
||||||
*/
|
|
||||||
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
|
||||||
try {
|
try {
|
||||||
await this.nestMailerService.sendMail({
|
await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import * as cookieParser from 'cookie-parser';
|
|||||||
import { VersioningType } from '@nestjs/common';
|
import { VersioningType } from '@nestjs/common';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
|
import { checkEnvironmentAuthProvider } from './utils';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
console.log(`Port: ${process.env.PORT}`);
|
console.log(`Port: ${process.env.PORT}`);
|
||||||
|
|
||||||
|
checkEnvironmentAuthProvider();
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdOn: 'desc',
|
createdOn: 'desc',
|
||||||
},
|
},
|
||||||
skip: 1,
|
skip: args.cursor ? 1 : 0,
|
||||||
take: args.take,
|
take: args.take,
|
||||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as O from 'fp-ts/Option';
|
|
||||||
import * as S from 'fp-ts/string';
|
|
||||||
import { pipe } from 'fp-ts/function';
|
|
||||||
import {
|
|
||||||
getAnnotatedRequiredRoles,
|
|
||||||
getGqlArg,
|
|
||||||
getUserFromGQLContext,
|
|
||||||
throwErr,
|
|
||||||
} from 'src/utils';
|
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
@@ -19,6 +9,10 @@ import {
|
|||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard which checks whether the caller of a GQL Operation
|
* A guard which checks whether the caller of a GQL Operation
|
||||||
@@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
|
|||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||||
TE.Do,
|
'requiresTeamRole',
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
|
||||||
|
|
||||||
TE.bindW('requiredRoles', () =>
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
pipe(
|
|
||||||
getAnnotatedRequiredRoles(this.reflector, context),
|
|
||||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('user', () =>
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
pipe(
|
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||||
getUserFromGQLContext(context),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('envID', () =>
|
const { id } = gqlExecCtx.getArgs<{ id: string }>();
|
||||||
pipe(
|
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
|
||||||
getGqlArg('id', context),
|
|
||||||
O.fromPredicate(S.isString),
|
|
||||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('membership', ({ envID, user }) =>
|
const teamEnvironment =
|
||||||
pipe(
|
await this.teamEnvironmentService.getTeamEnvironment(id);
|
||||||
this.teamEnvironmentService.getTeamEnvironment(envID),
|
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
|
|
||||||
TE.chainW((env) =>
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(env.teamID, user.uid),
|
|
||||||
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.map(({ membership, requiredRoles }) =>
|
const member = await this.teamService.getTeamMember(
|
||||||
requiredRoles.includes(membership.role),
|
teamEnvironment.right.teamID,
|
||||||
),
|
user.uid,
|
||||||
|
);
|
||||||
|
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
|
||||||
|
|
||||||
TE.getOrElse(throwErr),
|
return requireRoles.includes(member.role);
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateTeamEnvironmentArgs {
|
||||||
|
@Field({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team',
|
||||||
|
})
|
||||||
|
teamID: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class UpdateTeamEnvironmentArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID of the Team Environment',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
@Field({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
@Field({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string;
|
||||||
|
}
|
||||||
@@ -13,6 +13,11 @@ import { throwErr } from 'src/utils';
|
|||||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
CreateTeamEnvironmentArgs,
|
||||||
|
UpdateTeamEnvironmentArgs,
|
||||||
|
} from './input-type.args';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => 'TeamEnvironment')
|
@Resolver(() => 'TeamEnvironment')
|
||||||
@@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
createTeamEnvironment(
|
async createTeamEnvironment(
|
||||||
@Args({
|
@Args() args: CreateTeamEnvironmentArgs,
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string,
|
|
||||||
@Args({
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
teamID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string,
|
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return this.teamEnvironmentsService.createTeamEnvironment(
|
const teamEnvironment =
|
||||||
name,
|
await this.teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamID,
|
args.name,
|
||||||
variables,
|
args.teamID,
|
||||||
)();
|
args.variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||||
|
return teamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
deleteTeamEnvironment(
|
async deleteTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return pipe(
|
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
id,
|
||||||
TE.getOrElse(throwErr),
|
);
|
||||||
)();
|
|
||||||
|
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||||
|
return isDeleted.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
updateTeamEnvironment(
|
async updateTeamEnvironment(
|
||||||
@Args({
|
@Args()
|
||||||
name: 'id',
|
args: UpdateTeamEnvironmentArgs,
|
||||||
description: 'ID of the Team Environment',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
id: string,
|
|
||||||
@Args({
|
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string,
|
|
||||||
@Args({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string,
|
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const updatedTeamEnvironment =
|
||||||
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
await this.teamEnvironmentsService.updateTeamEnvironment(
|
||||||
TE.getOrElse(throwErr),
|
args.id,
|
||||||
)();
|
args.name,
|
||||||
|
args.variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
|
||||||
|
return updatedTeamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
deleteAllVariablesFromTeamEnvironment(
|
async deleteAllVariablesFromTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const teamEnvironment =
|
||||||
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
TE.getOrElse(throwErr),
|
id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||||
|
return teamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
createDuplicateEnvironment(
|
async createDuplicateEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
id,
|
||||||
TE.getOrElse(throwErr),
|
);
|
||||||
)();
|
|
||||||
|
if (E.isLeft(res)) throwErr(res.left);
|
||||||
|
return res.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
JSON_INVALID,
|
||||||
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
@@ -31,125 +35,81 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('TeamEnvironmentsService', () => {
|
describe('TeamEnvironmentsService', () => {
|
||||||
describe('getTeamEnvironment', () => {
|
describe('getTeamEnvironment', () => {
|
||||||
test('queries the db with the id', async () => {
|
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
|
teamEnvironment,
|
||||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
test('requests prisma to reject the query promise if not found', async () => {
|
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
teamEnvironment.id,
|
||||||
|
|
||||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
rejectOnNotFound: true,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
expect(result).toEqualRight(teamEnvironment);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a Some of the correct environment if exists', async () => {
|
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
|
'RejectOnNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
expect(result).toEqualSome(teamEnvironment);
|
);
|
||||||
});
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
|
||||||
test('should return a None if the environment does not exist', async () => {
|
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(result).toBeNone();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createTeamEnvironment', () => {
|
describe('createTeamEnvironment', () => {
|
||||||
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
test('should successfully create and return a new team environment given valid inputs', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqual(<TeamEnvironment>{
|
expect(result).toEqualRight({
|
||||||
id: teamEnvironment.id,
|
...teamEnvironment,
|
||||||
name: teamEnvironment.name,
|
|
||||||
teamID: teamEnvironment.teamID,
|
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject if given team ID is invalid', async () => {
|
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
|
'12',
|
||||||
|
teamEnvironment.teamID,
|
||||||
|
JSON.stringify(teamEnvironment.variables),
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
teamEnvironment.name,
|
|
||||||
'invalidteamid',
|
|
||||||
JSON.stringify(teamEnvironment.variables),
|
|
||||||
),
|
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should reject if provided team environment name is not a string', async () => {
|
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
null as any,
|
|
||||||
teamEnvironment.teamID,
|
|
||||||
JSON.stringify(teamEnvironment.variables),
|
|
||||||
),
|
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should reject if provided variable is not a string', async () => {
|
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
teamEnvironment.name,
|
|
||||||
teamEnvironment.teamID,
|
|
||||||
null as any,
|
|
||||||
),
|
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
result,
|
{
|
||||||
|
...teamEnvironment,
|
||||||
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteTeamEnvironment', () => {
|
describe('deleteTeamEnvironment', () => {
|
||||||
test('should resolve to true given a valid team environment ID', async () => {
|
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(true);
|
expect(result).toEqualRight(true);
|
||||||
});
|
});
|
||||||
@@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||||
@@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateVariablesInTeamEnvironment', () => {
|
describe('updateVariablesInTeamEnvironment', () => {
|
||||||
test('should add new variable to a team environment', async () => {
|
test('should successfully add new variable to a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }],
|
variables: [{ key: 'value' }],
|
||||||
@@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add new variable to already existing list of variables in a team environment', async () => {
|
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||||
@@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should edit existing variables in a team environment', async () => {
|
test('should successfully edit existing variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '1234' }],
|
variables: [{ key: '1234' }],
|
||||||
@@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '1234' }]),
|
JSON.stringify([{ key: '1234' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should delete existing variable in a team environment', async () => {
|
test('should successfully edit name of an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
|
||||||
teamEnvironment.id,
|
|
||||||
teamEnvironment.name,
|
|
||||||
JSON.stringify([{}]),
|
|
||||||
)();
|
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
|
||||||
...teamEnvironment,
|
|
||||||
variables: JSON.stringify([{}]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should edit name of an existing team environment', async () => {
|
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '123' }],
|
variables: [{ key: '123' }],
|
||||||
@@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '123' }]),
|
JSON.stringify([{ key: '123' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||||
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
|
'12',
|
||||||
|
JSON.stringify([{ key: 'value' }]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||||
test('should delete all variables in a team environment', async () => {
|
test('should successfully delete all variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -345,33 +300,33 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createDuplicateEnvironment', () => {
|
describe('createDuplicateEnvironment', () => {
|
||||||
test('should duplicate an existing team environment', async () => {
|
test('should successfully duplicate an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||||
teamEnvironment,
|
teamEnvironment,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -382,19 +337,19 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
{
|
{
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
variables: JSON.stringify([{}]),
|
variables: JSON.stringify([{}]),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as A from 'fp-ts/Array';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { isValidLength } from 'src/utils';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamEnvironmentsService {
|
export class TeamEnvironmentsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,219 +16,218 @@ export class TeamEnvironmentsService {
|
|||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getTeamEnvironment(id: string) {
|
TITLE_LENGTH = 3;
|
||||||
return TO.tryCatch(() =>
|
|
||||||
this.prisma.teamEnvironment.findFirst({
|
/**
|
||||||
where: { id },
|
* TeamEnvironments are saved in the DB in the following way
|
||||||
|
* [{ key: value }, { key: value },....]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typecast a database TeamEnvironment to a TeamEnvironment model
|
||||||
|
* @param teamEnvironment database TeamEnvironment
|
||||||
|
* @returns TeamEnvironment model
|
||||||
|
*/
|
||||||
|
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
|
||||||
|
return {
|
||||||
|
id: teamEnvironment.id,
|
||||||
|
name: teamEnvironment.name,
|
||||||
|
teamID: teamEnvironment.teamID,
|
||||||
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details of a TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async getTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const teamEnvironment =
|
||||||
|
await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return E.right(teamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param name name of new TeamEnvironment
|
||||||
|
* @param teamID teamID of new TeamEnvironment
|
||||||
|
* @param variables JSONified string of contents of new TeamEnvironment
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||||
|
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
|
||||||
|
const result = await this.prisma.teamEnvironment.create({
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
teamID: teamID,
|
||||||
|
variables: JSON.parse(variables),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdTeamEnvironment = this.cast(result);
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${createdTeamEnvironment.teamID}/created`,
|
||||||
|
createdTeamEnvironment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(createdTeamEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @returns Either of boolean or error message
|
||||||
|
*/
|
||||||
|
async deleteTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.teamEnvironment.delete({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedTeamEnvironment = this.cast(result);
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${deletedTeamEnvironment.teamID}/deleted`,
|
||||||
|
deletedTeamEnvironment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @param name TeamEnvironment name
|
||||||
|
* @param variables JSONified string of contents of new TeamEnvironment
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||||
|
try {
|
||||||
|
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
|
||||||
|
const result = await this.prisma.teamEnvironment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
variables: JSON.parse(variables),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedTeamEnvironment = this.cast(result);
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${updatedTeamEnvironment.teamID}/updated`,
|
||||||
|
updatedTeamEnvironment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(updatedTeamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear contents of a TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.teamEnvironment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
variables: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamEnvironment = this.cast(result);
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
|
teamEnvironment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(teamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a duplicate of a existing TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async createDuplicateEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const environment = await this.prisma.teamEnvironment.findFirst({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
rejectOnNotFound: true,
|
rejectOnNotFound: true,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
const result = await this.prisma.teamEnvironment.create({
|
||||||
|
data: {
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: environment.variables as Prisma.JsonArray,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicatedTeamEnvironment = this.cast(result);
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${duplicatedTeamEnvironment.teamID}/created`,
|
||||||
|
duplicatedTeamEnvironment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(duplicatedTeamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createTeamEnvironment(name: string, teamID: string, variables: string) {
|
/**
|
||||||
return pipe(
|
* Fetch all TeamEnvironments of a team.
|
||||||
() =>
|
*
|
||||||
this.prisma.teamEnvironment.create({
|
* @param teamID teamID of new TeamEnvironment
|
||||||
data: {
|
* @returns List of TeamEnvironments
|
||||||
name: name,
|
*/
|
||||||
teamID: teamID,
|
async fetchAllTeamEnvironments(teamID: string) {
|
||||||
variables: JSON.parse(variables),
|
const result = await this.prisma.teamEnvironment.findMany({
|
||||||
},
|
where: {
|
||||||
}),
|
teamID: teamID,
|
||||||
T.chainFirst(
|
},
|
||||||
(environment) => () =>
|
});
|
||||||
this.pubsub.publish(
|
const teamEnvironments = result.map((item) => {
|
||||||
`team_environment/${environment.teamID}/created`,
|
return this.cast(item);
|
||||||
<TeamEnvironment>{
|
});
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
T.map((data) => {
|
|
||||||
return <TeamEnvironment>{
|
|
||||||
id: data.id,
|
|
||||||
name: data.name,
|
|
||||||
teamID: data.teamID,
|
|
||||||
variables: JSON.stringify(data.variables),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTeamEnvironment(id: string) {
|
return teamEnvironments;
|
||||||
return pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamEnvironment.delete({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${environment.teamID}/deleted`,
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map((data) => true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTeamEnvironment(id: string, name: string, variables: string) {
|
|
||||||
return pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamEnvironment.update({
|
|
||||||
where: { id: id },
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
variables: JSON.parse(variables),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${environment.teamID}/updated`,
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAllVariablesFromTeamEnvironment(id: string) {
|
|
||||||
return pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamEnvironment.update({
|
|
||||||
where: { id: id },
|
|
||||||
data: {
|
|
||||||
variables: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${environment.teamID}/updated`,
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createDuplicateEnvironment(id: string) {
|
|
||||||
return pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamEnvironment.findFirst({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
rejectOnNotFound: true,
|
|
||||||
}),
|
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
|
||||||
TE.chain((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.prisma.teamEnvironment.create({
|
|
||||||
data: {
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: environment.variables as Prisma.JsonArray,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${environment.teamID}/created`,
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAllTeamEnvironments(teamID: string) {
|
|
||||||
return pipe(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamEnvironment.findMany({
|
|
||||||
where: {
|
|
||||||
teamID: teamID,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
T.map(
|
|
||||||
A.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
|
|||||||
description: 'Returns all Team Environments for the given Team',
|
description: 'Returns all Team Environments for the given Team',
|
||||||
})
|
})
|
||||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||||
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateTeamInvitationArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team ID to invite from',
|
||||||
|
})
|
||||||
|
teamID: string;
|
||||||
|
|
||||||
|
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
|
||||||
|
inviteeEmail: string;
|
||||||
|
|
||||||
|
@Field(() => TeamMemberRole, {
|
||||||
|
name: 'inviteeRole',
|
||||||
|
description: 'Role to be given to the user',
|
||||||
|
})
|
||||||
|
inviteeRole: TeamMemberRole;
|
||||||
|
}
|
||||||
@@ -12,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
|
|||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { pipe } from 'fp-ts/function';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { EmailCodec } from 'src/types/Email';
|
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||||
import {
|
|
||||||
INVALID_EMAIL,
|
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
|
||||||
USER_NOT_FOUND,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { User } from 'src/user/user.model';
|
import { User } from 'src/user/user.model';
|
||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
@@ -36,6 +31,8 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { CreateTeamInvitationArgs } from './input-type.args';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => TeamInvitation)
|
@Resolver(() => TeamInvitation)
|
||||||
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
|
|||||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||||
teamInvitation(
|
async teamInvitation(
|
||||||
@GqlUser() user: User,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
description: 'ID of the Team Invitation to lookup',
|
description: 'ID of the Team Invitation to lookup',
|
||||||
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.getInvitation(
|
||||||
this.teamInvitationService.getInvitation(inviteID),
|
inviteID,
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
);
|
||||||
TE.chainW(
|
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
TE.fromPredicate(
|
return teamInvitation.value;
|
||||||
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.getOrElse(throwErr),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamInvitation, {
|
@Mutation(() => TeamInvitation, {
|
||||||
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
createTeamInvitation(
|
async createTeamInvitation(
|
||||||
@GqlUser()
|
@GqlUser() user: AuthUser,
|
||||||
user: User,
|
@Args() args: CreateTeamInvitationArgs,
|
||||||
|
|
||||||
@Args({
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team ID to invite from',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
teamID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeEmail',
|
|
||||||
description: 'Email of the user to invite',
|
|
||||||
})
|
|
||||||
inviteeEmail: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeRole',
|
|
||||||
type: () => TeamMemberRole,
|
|
||||||
description: 'Role to be given to the user',
|
|
||||||
})
|
|
||||||
inviteeRole: TeamMemberRole,
|
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.createInvitation(
|
||||||
TE.Do,
|
user,
|
||||||
|
args.teamID,
|
||||||
|
args.inviteeEmail,
|
||||||
|
args.inviteeRole,
|
||||||
|
);
|
||||||
|
|
||||||
// Validate email
|
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
||||||
TE.bindW('email', () =>
|
return teamInvitation.right;
|
||||||
pipe(
|
|
||||||
EmailCodec.decode(inviteeEmail),
|
|
||||||
TE.fromEither,
|
|
||||||
TE.mapLeft(() => INVALID_EMAIL),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validate and get Team
|
|
||||||
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
|
|
||||||
|
|
||||||
// Create team
|
|
||||||
TE.chainW(({ email, team }) =>
|
|
||||||
this.teamInvitationService.createInvitation(
|
|
||||||
user,
|
|
||||||
team,
|
|
||||||
email,
|
|
||||||
inviteeRole,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// If failed, throw err (so the message is passed) else return value
|
|
||||||
TE.getOrElse(throwErr),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
revokeTeamInvitation(
|
async revokeTeamInvitation(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<true> {
|
): Promise<true> {
|
||||||
return pipe(
|
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
||||||
this.teamInvitationService.revokeInvitation(inviteID),
|
inviteID,
|
||||||
TE.map(() => true as const),
|
);
|
||||||
TE.getOrElse(throwErr),
|
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
||||||
)();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamMember, {
|
@Mutation(() => TeamMember, {
|
||||||
description: 'Accept an Invitation',
|
description: 'Accept an Invitation',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||||
acceptTeamInvitation(
|
async acceptTeamInvitation(
|
||||||
@GqlUser() user: User,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
return pipe(
|
const teamMember = await this.teamInvitationService.acceptInvitation(
|
||||||
this.teamInvitationService.acceptInvitation(inviteID, user),
|
inviteID,
|
||||||
TE.getOrElse(throwErr),
|
user,
|
||||||
)();
|
);
|
||||||
|
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||||
|
return teamMember.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { Team, TeamMemberRole } from 'src/team/team.model';
|
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
||||||
import { Email } from 'src/types/Email';
|
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import {
|
import {
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
|
TEAM_INVALID_ID,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamInvitation } from './team-invitation.model';
|
import { TeamInvitation } from './team-invitation.model';
|
||||||
import { MailerService } from 'src/mailer/mailer.service';
|
import { MailerService } from 'src/mailer/mailer.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { validateEmail } from '../utils';
|
import { validateEmail } from '../utils';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -32,38 +30,37 @@ export class TeamInvitationService {
|
|||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {
|
) {}
|
||||||
this.getInvitation = this.getInvitation.bind(this);
|
|
||||||
|
/**
|
||||||
|
* Cast a DBTeamInvitation to a TeamInvitation
|
||||||
|
* @param dbTeamInvitation database TeamInvitation
|
||||||
|
* @returns TeamInvitation model
|
||||||
|
*/
|
||||||
|
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
||||||
|
return {
|
||||||
|
...dbTeamInvitation,
|
||||||
|
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
/**
|
||||||
return pipe(
|
* Get the team invite
|
||||||
() =>
|
* @param inviteID invite id
|
||||||
this.prisma.teamInvitation.findUnique({
|
* @returns an Option of team invitation or none
|
||||||
where: {
|
*/
|
||||||
id: inviteID,
|
async getInvitation(inviteID: string) {
|
||||||
},
|
try {
|
||||||
}),
|
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||||
TO.fromTask,
|
where: {
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
id: inviteID,
|
||||||
TO.map((x) => x as TeamInvitation),
|
},
|
||||||
);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getInvitationWithEmail(email: Email, team: Team) {
|
return O.some(this.cast(dbInvitation));
|
||||||
return pipe(
|
} catch (e) {
|
||||||
() =>
|
return O.none;
|
||||||
this.prisma.teamInvitation.findUnique({
|
}
|
||||||
where: {
|
|
||||||
teamID_inviteeEmail: {
|
|
||||||
inviteeEmail: email,
|
|
||||||
teamID: team.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
TO.fromTask,
|
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,211 +89,162 @@ export class TeamInvitationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createInvitation(
|
/**
|
||||||
creator: User,
|
* Create a team invitation
|
||||||
team: Team,
|
* @param creator creator of the invitation
|
||||||
inviteeEmail: Email,
|
* @param teamID team id
|
||||||
|
* @param inviteeEmail invitee email
|
||||||
|
* @param inviteeRole invitee role
|
||||||
|
* @returns an Either of team invitation or error message
|
||||||
|
*/
|
||||||
|
async createInvitation(
|
||||||
|
creator: AuthUser,
|
||||||
|
teamID: string,
|
||||||
|
inviteeEmail: string,
|
||||||
inviteeRole: TeamMemberRole,
|
inviteeRole: TeamMemberRole,
|
||||||
) {
|
) {
|
||||||
return pipe(
|
// validate email
|
||||||
// Perform all validation checks
|
const isEmailValid = validateEmail(inviteeEmail);
|
||||||
TE.sequenceArray([
|
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||||
// creator should be a TeamMember
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Invitee should not be a team member
|
// team ID should valid
|
||||||
pipe(
|
const team = await this.teamService.getTeamWithID(teamID);
|
||||||
async () => await this.userService.findUserByEmail(inviteeEmail),
|
if (!team) return E.left(TEAM_INVALID_ID);
|
||||||
TO.foldW(
|
|
||||||
() => TE.right(undefined), // If no user, short circuit to completion
|
|
||||||
(user) =>
|
|
||||||
pipe(
|
|
||||||
// If user is found, check if team member
|
|
||||||
this.teamService.getTeamMemberTE(team.id, user.uid),
|
|
||||||
TE.foldW(
|
|
||||||
() => TE.right(undefined), // Not team-member, this is good
|
|
||||||
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Should not have an existing invite
|
// invitation creator should be a TeamMember
|
||||||
pipe(
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
this.getInvitationWithEmail(inviteeEmail, team),
|
team.id,
|
||||||
TE.fromTaskOption(() => null),
|
creator.uid,
|
||||||
TE.swap,
|
|
||||||
TE.map(constVoid),
|
|
||||||
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// Create the invitation
|
|
||||||
TE.chainTaskK(
|
|
||||||
() => () =>
|
|
||||||
this.prisma.teamInvitation.create({
|
|
||||||
data: {
|
|
||||||
teamID: team.id,
|
|
||||||
inviteeEmail,
|
|
||||||
inviteeRole,
|
|
||||||
creatorUid: creator.uid,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Send email, this is a side effect
|
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
pipe(
|
|
||||||
this.mailerService.sendMail(inviteeEmail, {
|
|
||||||
template: 'team-invitation',
|
|
||||||
variables: {
|
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
|
||||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
|
|
||||||
invite_team_name: team.name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Send PubSub topic
|
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
TE.fromTask(async () => {
|
|
||||||
const inv: TeamInvitation = {
|
|
||||||
id: invitation.id,
|
|
||||||
teamID: invitation.teamID,
|
|
||||||
creatorUid: invitation.creatorUid,
|
|
||||||
inviteeEmail: invitation.inviteeEmail,
|
|
||||||
inviteeRole: TeamMemberRole[invitation.inviteeRole],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Map to model type
|
|
||||||
TE.map((x) => x as TeamInvitation),
|
|
||||||
);
|
);
|
||||||
}
|
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
|
||||||
revokeInvitation(inviteID: string) {
|
// Checking to see if the invitee is already part of the team or not
|
||||||
return pipe(
|
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
||||||
// Make sure invite exists
|
if (O.isSome(inviteeUser)) {
|
||||||
this.getInvitation(inviteID),
|
// invitee should not already a member
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
|
team.id,
|
||||||
|
inviteeUser.value.uid,
|
||||||
|
);
|
||||||
|
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete team invitation
|
// check invitee already invited earlier or not
|
||||||
TE.chainTaskK(
|
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
||||||
() => () =>
|
inviteeEmail,
|
||||||
this.prisma.teamInvitation.delete({
|
team.id,
|
||||||
where: {
|
|
||||||
id: inviteID,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Emit Pubsub Event
|
|
||||||
TE.chainFirst((invitation) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team/${invitation.teamID}/invite_removed`,
|
|
||||||
invitation.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// We are not returning anything
|
|
||||||
TE.map(constVoid),
|
|
||||||
);
|
);
|
||||||
}
|
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
||||||
|
|
||||||
getAllInvitationsInTeam(team: Team) {
|
// create the invitation
|
||||||
return pipe(
|
const dbInvitation = await this.prisma.teamInvitation.create({
|
||||||
() =>
|
data: {
|
||||||
this.prisma.teamInvitation.findMany({
|
teamID: team.id,
|
||||||
where: {
|
inviteeEmail,
|
||||||
teamID: team.id,
|
inviteeRole,
|
||||||
},
|
creatorUid: creator.uid,
|
||||||
}),
|
},
|
||||||
T.map((x) => x as TeamInvitation[]),
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptInvitation(inviteID: string, acceptedBy: User) {
|
await this.mailerService.sendEmail(inviteeEmail, {
|
||||||
return pipe(
|
template: 'team-invitation',
|
||||||
TE.Do,
|
variables: {
|
||||||
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||||
|
invite_team_name: team.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// First get the invitation
|
const invitation = this.cast(dbInvitation);
|
||||||
TE.bindW('invitation', () =>
|
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
||||||
pipe(
|
|
||||||
this.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validation checks
|
return E.right(invitation);
|
||||||
TE.chainFirstW(({ invitation }) =>
|
|
||||||
TE.sequenceArray([
|
|
||||||
// Make sure the invited user is not part of the team
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
|
|
||||||
TE.swap,
|
|
||||||
TE.bimap(
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER,
|
|
||||||
constVoid, // The return type is ignored
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Make sure the invited user and accepting user has the same email
|
|
||||||
pipe(
|
|
||||||
undefined,
|
|
||||||
TE.fromPredicate(
|
|
||||||
(a) => acceptedBy.email === invitation.inviteeEmail,
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add the team member
|
|
||||||
// TODO: Somehow bring subscriptions to this ?
|
|
||||||
TE.bindW('teamMember', ({ invitation }) =>
|
|
||||||
pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.teamService.addMemberToTeam(
|
|
||||||
invitation.teamID,
|
|
||||||
acceptedBy.uid,
|
|
||||||
invitation.inviteeRole,
|
|
||||||
),
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
|
|
||||||
|
|
||||||
TE.map(({ teamMember }) => teamMember),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the count invitations for a given team.
|
* Revoke a team invitation
|
||||||
* @param teamID team id
|
* @param inviteID invite id
|
||||||
* @returns a count team invitations for a team
|
* @returns an Either of true or error message
|
||||||
*/
|
*/
|
||||||
async getAllTeamInvitations(teamID: string) {
|
async revokeInvitation(inviteID: string) {
|
||||||
const invitations = await this.prisma.teamInvitation.findMany({
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.prisma.teamInvitation.delete({
|
||||||
|
where: {
|
||||||
|
id: inviteID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team/${invitation.value.teamID}/invite_removed`,
|
||||||
|
invitation.value.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a team invitation
|
||||||
|
* @param inviteID invite id
|
||||||
|
* @param acceptedBy user who accepted the invitation
|
||||||
|
* @returns an Either of team member or error message
|
||||||
|
*/
|
||||||
|
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
|
||||||
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// make sure the user is not already a member of the team
|
||||||
|
const teamMemberInvitee = await this.teamService.getTeamMember(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
);
|
||||||
|
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
|
||||||
|
// make sure the user is the same as the invitee
|
||||||
|
if (
|
||||||
|
acceptedBy.email.toLowerCase() !==
|
||||||
|
invitation.value.inviteeEmail.toLowerCase()
|
||||||
|
)
|
||||||
|
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
|
|
||||||
|
// add the user to the team
|
||||||
|
let teamMember: TeamMember;
|
||||||
|
try {
|
||||||
|
teamMember = await this.teamService.addMemberToTeam(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
invitation.value.inviteeRole,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.revokeInvitation(inviteID);
|
||||||
|
|
||||||
|
return E.right(teamMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all team invitations for a given team.
|
||||||
|
* @param teamID team id
|
||||||
|
* @returns array of team invitations for a team
|
||||||
|
*/
|
||||||
|
async getTeamInvitations(teamID: string) {
|
||||||
|
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
||||||
|
this.cast(dbInvitation),
|
||||||
|
);
|
||||||
|
|
||||||
return invitations;
|
return invitations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { pipe } from 'fp-ts/function';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
TEAM_NOT_REQUIRED_ROLE,
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamMemberRole } from 'src/team/team.model';
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This guard only allows team owner to execute the resolver
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
// Get user
|
||||||
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get the invite
|
// Get the invite
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW((inviteID) =>
|
|
||||||
pipe(
|
|
||||||
this.teamInviteService.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
pipe(
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
gqlCtx.getContext().req.user,
|
|
||||||
O.fromNullable,
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('userMember', ({ invite, user }) =>
|
// Fetch team member details of this user
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
),
|
invitation.value.teamID,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
TE.chainW(
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
TE.fromPredicate(
|
if (teamMember.role !== TeamMemberRole.OWNER)
|
||||||
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
||||||
() => TEAM_NOT_REQUIRED_ROLE,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.fold(
|
return true;
|
||||||
(err) => throwErr(err),
|
|
||||||
() => T.of(true),
|
|
||||||
),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NOT_VALID_VIEWER,
|
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This guard only allows user to execute the resolver
|
||||||
|
* 1. If user is invitee, allow
|
||||||
|
* 2. Or else, if user is team member, allow
|
||||||
|
*
|
||||||
|
* TLDR: Allow if user is invitee or team member
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteViewerGuard implements CanActivate {
|
export class TeamInviteViewerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get GQL Context
|
// Get user
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get user
|
// Get the invite
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getContext().req.user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get the invite
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
pipe(
|
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Check if the user and the invite email match, else if we can resolver the user as a team member
|
// Check if the user and the invite email match, else if user is a team member
|
||||||
// any better solution ?
|
if (
|
||||||
TE.chainW(({ user, invite }) =>
|
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
) {
|
||||||
? TE.of(true)
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
: pipe(
|
invitation.value.teamID,
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
user.uid,
|
||||||
TE.map(() => true),
|
);
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.mapLeft((e) =>
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
}
|
||||||
),
|
|
||||||
|
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
return true;
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
@@ -24,44 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
|
|||||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL Context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get execution context
|
// Get user
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get user
|
// Get the invite
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getContext().req.user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get invite
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
pipe(
|
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Check if the emails match
|
if (
|
||||||
TE.chainW(
|
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
TE.fromPredicate(
|
) {
|
||||||
({ user, invite }) => user.email === invite.inviteeEmail,
|
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
}
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Fold it to a promise
|
return true;
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
|||||||
complexity: 10,
|
complexity: 10,
|
||||||
})
|
})
|
||||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||||
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
return this.teamInviteService.getTeamInvitations(team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ beforeEach(() => {
|
|||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
describe('UserHistoryService', () => {
|
describe('UserHistoryService', () => {
|
||||||
describe('fetchUserHistory', () => {
|
describe('fetchUserHistory', () => {
|
||||||
test('Should return a list of users REST history if exists', async () => {
|
test('Should return a list of users REST history if exists', async () => {
|
||||||
@@ -400,7 +402,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn: date,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,7 +412,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn: date,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import * as E from 'fp-ts/Either';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { TeamMemberRole } from './team/team.model';
|
import { TeamMemberRole } from './team/team.model';
|
||||||
import { User } from './user/user.model';
|
import { User } from './user/user.model';
|
||||||
import { 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -152,3 +158,31 @@ export function isValidLength(title: string, length: number) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called by bootstrap() in main.ts
|
||||||
|
* It checks if the "VITE_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')) {
|
||||||
|
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||||
|
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||||
|
',',
|
||||||
|
).map((provider) => provider.toLocaleUpperCase());
|
||||||
|
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||||
|
(provider: string) => provider.toLocaleUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const givenAuthProvider of givenAuthProviders) {
|
||||||
|
if (!supportedAuthProviders.includes(givenAuthProvider)) {
|
||||||
|
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
- Demonstrating empathy and kindness toward other people
|
|
||||||
- Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
- Giving and gracefully accepting constructive feedback
|
|
||||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
- Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
- The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
- Public or private harassment
|
|
||||||
- Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
- Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
support@hoppscotch.io.
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
<div align="center">
|
|
||||||
<a href="https://hoppscotch.io">
|
|
||||||
<img
|
|
||||||
src="https://avatars.githubusercontent.com/u/56705483"
|
|
||||||
alt="Hoppscotch Logo"
|
|
||||||
height="64"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
||||||
|
|
||||||
</div>
|
A CLI to run Hoppscotch Test Scripts in CI environments.
|
||||||
|
|
||||||
A CLI to run Hoppscotch test scripts in CI environments.
|
|
||||||
|
|
||||||
### **Commands:**
|
### **Commands:**
|
||||||
|
|
||||||
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
||||||
|
|
||||||
### **Usage:**
|
### **Usage:**
|
||||||
```
|
|
||||||
|
```bash
|
||||||
hopp [options or commands] arguments
|
hopp [options or commands] arguments
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Options:**
|
### **Options:**
|
||||||
|
|
||||||
- `-v`, `--ver`: see the current version of the CLI
|
- `-v`, `--ver`: see the current version of the CLI
|
||||||
- `-h`, `--help`: display help for command
|
- `-h`, `--help`: display help for command
|
||||||
|
|
||||||
@@ -45,17 +35,21 @@ hopp [options or commands] arguments
|
|||||||
- Executes and outputs test-script response.
|
- Executes and outputs test-script response.
|
||||||
|
|
||||||
#### Options:
|
#### Options:
|
||||||
|
|
||||||
##### `-e <file_path>` / `--env <file_path>`
|
##### `-e <file_path>` / `--env <file_path>`
|
||||||
|
|
||||||
- Accepts path to env.json with contents in below format:
|
- Accepts path to env.json with contents in below format:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ENV1":"value1",
|
"ENV1":"value1",
|
||||||
"ENV2":"value2"
|
"ENV2":"value2"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||||
|
|
||||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -75,4 +69,59 @@ npm i -g @hoppscotch/cli
|
|||||||
|
|
||||||
## **Contributing:**
|
## **Contributing:**
|
||||||
|
|
||||||
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**
|
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||||
|
email, or any other method with the owners of this repository before making a change.
|
||||||
|
|
||||||
|
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||||
|
build.
|
||||||
|
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||||
|
variables, exposed ports, useful file locations and container parameters.
|
||||||
|
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||||
|
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
|
||||||
|
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
|
||||||
|
do not have permission to do that, you may request the second reviewer merge it for you.
|
||||||
|
|
||||||
|
## Set Up The Development Environment
|
||||||
|
|
||||||
|
1. After cloning the repository, execute the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In order to test locally, you can use two types of package linking:
|
||||||
|
|
||||||
|
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm link @hoppscotch/cli
|
||||||
|
|
||||||
|
// Then to use or test the CLI:
|
||||||
|
pnpm exec hopp
|
||||||
|
|
||||||
|
// After testing, to remove the package linking:
|
||||||
|
pnpm rm @hoppscotch/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The 'global' way (warning: this might override the globally installed CLI, if exists):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pnpm link --global
|
||||||
|
|
||||||
|
// Then to use or test the CLI:
|
||||||
|
hopp
|
||||||
|
|
||||||
|
// After testing, to remove the package linking:
|
||||||
|
sudo pnpm rm --global @hoppscotch/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
3. To use the Typescript watch scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
@@ -30,8 +29,18 @@ module.exports = {
|
|||||||
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
"prettier/prettier":
|
"prettier/prettier": [
|
||||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
"vue/multi-word-component-names": "off",
|
"vue/multi-word-component-names": "off",
|
||||||
"vue/no-side-effects-in-computed-properties": "off",
|
"vue/no-side-effects-in-computed-properties": "off",
|
||||||
"import/no-named-as-default": "off",
|
"import/no-named-as-default": "off",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||||
|
After Width: | Height: | Size: 337 B |
@@ -4,6 +4,7 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
|
@apply overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -165,12 +166,6 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-icon {
|
|
||||||
@apply transition;
|
|
||||||
@apply inline-flex;
|
|
||||||
@apply items-center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -189,10 +184,11 @@ a {
|
|||||||
@apply border-solid border-dividerDark;
|
@apply border-solid border-dividerDark;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply shadow-lg;
|
@apply shadow-lg;
|
||||||
|
@apply max-w-[45vw] #{!important};
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
@apply max-h-56;
|
@apply max-h-[45vh];
|
||||||
@apply items-stretch;
|
@apply items-stretch;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-secondary text-body;
|
@apply text-secondary text-body;
|
||||||
@@ -200,6 +196,10 @@ a {
|
|||||||
@apply leading-normal;
|
@apply leading-normal;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
@apply block #{!important};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -215,6 +215,7 @@ a {
|
|||||||
|
|
||||||
[data-v-tippy] {
|
[data-v-tippy] {
|
||||||
@apply flex flex-1;
|
@apply flex flex-1;
|
||||||
|
@apply truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
[interactive] > div {
|
[interactive] > div {
|
||||||
@@ -325,7 +326,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e313"];
|
@apply after:content-["\e5cf"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,6 +481,10 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-scroller {
|
||||||
|
@apply overscroll-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -567,3 +572,11 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gql-operation-not-highlight {
|
||||||
|
@apply opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gql-operation-highlight {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter", sans-serif;
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
--font-mono: "Roboto Mono", monospace;
|
--font-icon: "Material Symbols Rounded Variable";
|
||||||
--font-icon: "Material Icons";
|
--font-mono: "Roboto Mono Variable", monospace;
|
||||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
--font-size-body: 0.75rem;
|
||||||
|
--font-size-tiny: 0.688rem;
|
||||||
|
--line-height-body: 1rem;
|
||||||
|
--upper-primary-sticky-fold: 4.125rem;
|
||||||
|
--upper-secondary-sticky-fold: 6.188rem;
|
||||||
|
--upper-tertiary-sticky-fold: 8.25rem;
|
||||||
|
--upper-mobile-primary-sticky-fold: 6.625rem;
|
||||||
|
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
||||||
|
--upper-mobile-sticky-fold: 10.75rem;
|
||||||
|
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||||
|
--lower-primary-sticky-fold: 3rem;
|
||||||
|
--lower-secondary-sticky-fold: 5.063rem;
|
||||||
|
--lower-tertiary-sticky-fold: 7.125rem;
|
||||||
|
--sidebar-primary-sticky-fold: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-theme {
|
@mixin dark-theme {
|
||||||
@@ -200,8 +213,8 @@
|
|||||||
:root {
|
:root {
|
||||||
@include base-theme;
|
@include base-theme;
|
||||||
@include dark-theme;
|
@include dark-theme;
|
||||||
@include green-theme;
|
|
||||||
@include dark-editor-theme;
|
@include dark-editor-theme;
|
||||||
|
@include green-theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light {
|
:root.light {
|
||||||
@@ -257,63 +270,3 @@
|
|||||||
:root[data-accent="yellow"] {
|
:root[data-accent="yellow"] {
|
||||||
@include yellow-theme;
|
@include yellow-theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin font-small {
|
|
||||||
--font-size-body: 0.75rem;
|
|
||||||
--line-height-body: 1rem;
|
|
||||||
--upper-primary-sticky-fold: 4.125rem;
|
|
||||||
--upper-secondary-sticky-fold: 6.188rem;
|
|
||||||
--upper-tertiary-sticky-fold: 8.25rem;
|
|
||||||
--upper-mobile-primary-sticky-fold: 6.625rem;
|
|
||||||
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
|
||||||
--upper-mobile-sticky-fold: 10.75rem;
|
|
||||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
|
||||||
--lower-primary-sticky-fold: 3rem;
|
|
||||||
--lower-secondary-sticky-fold: 5.063rem;
|
|
||||||
--lower-tertiary-sticky-fold: 7.125rem;
|
|
||||||
--sidebar-primary-sticky-fold: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin font-medium {
|
|
||||||
--font-size-body: 0.875rem;
|
|
||||||
--line-height-body: 1.25rem;
|
|
||||||
--upper-primary-sticky-fold: 4.375rem;
|
|
||||||
--upper-secondary-sticky-fold: 6.688rem;
|
|
||||||
--upper-tertiary-sticky-fold: 9rem;
|
|
||||||
--upper-mobile-primary-sticky-fold: 7.125rem;
|
|
||||||
--upper-mobile-secondary-sticky-fold: 9.438rem;
|
|
||||||
--upper-mobile-sticky-fold: 11.75rem;
|
|
||||||
--upper-mobile-tertiary-sticky-fold: 9rem;
|
|
||||||
--lower-primary-sticky-fold: 3.25rem;
|
|
||||||
--lower-secondary-sticky-fold: 5.563rem;
|
|
||||||
--lower-tertiary-sticky-fold: 7.875rem;
|
|
||||||
--sidebar-primary-sticky-fold: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin font-large {
|
|
||||||
--font-size-body: 1rem;
|
|
||||||
--line-height-body: 1.5rem;
|
|
||||||
--upper-primary-sticky-fold: 4.625rem;
|
|
||||||
--upper-secondary-sticky-fold: 7.188rem;
|
|
||||||
--upper-tertiary-sticky-fold: 9.75rem;
|
|
||||||
--upper-mobile-primary-sticky-fold: 7.625rem;
|
|
||||||
--upper-mobile-secondary-sticky-fold: 10.188rem;
|
|
||||||
--upper-mobile-sticky-fold: 12.75rem;
|
|
||||||
--upper-mobile-tertiary-sticky-fold: 9.75rem;
|
|
||||||
--lower-primary-sticky-fold: 3.5rem;
|
|
||||||
--lower-secondary-sticky-fold: 6.063rem;
|
|
||||||
--lower-tertiary-sticky-fold: 8.625rem;
|
|
||||||
--sidebar-primary-sticky-fold: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-font-size="small"] {
|
|
||||||
@include font-small;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-font-size="medium"] {
|
|
||||||
@include font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-font-size="large"] {
|
|
||||||
@include font-large;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"choose_file": "Choose a file",
|
"choose_file": "Choose a file",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"clear_history": "Clear All History",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Open workspace",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"prettify": "Prettify",
|
"prettify": "Prettify",
|
||||||
|
"rename": "Rename",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -67,6 +69,8 @@
|
|||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
||||||
"invite_your_friends": "Invite your friends",
|
"invite_your_friends": "Invite your friends",
|
||||||
|
"social_links": "Social links",
|
||||||
|
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||||
"join_discord_community": "Join our Discord community",
|
"join_discord_community": "Join our Discord community",
|
||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
@@ -131,6 +135,7 @@
|
|||||||
"renamed": "Collection renamed",
|
"renamed": "Collection renamed",
|
||||||
"request_in_use": "Request in use",
|
"request_in_use": "Request in use",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
|
"save_to_collection": "Save to Collection",
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location",
|
||||||
"select_team": "Select a team",
|
"select_team": "Select a team",
|
||||||
@@ -148,8 +153,15 @@
|
|||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
|
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||||
|
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
|
"context_menu": {
|
||||||
|
"set_environment_variable": "Set as variable",
|
||||||
|
"add_parameters": "Add to parameters",
|
||||||
|
"open_request_in_new_tab": "Open request in new tab"
|
||||||
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Header {count}",
|
"header": "Header {count}",
|
||||||
"message": "Message {count}",
|
"message": "Message {count}",
|
||||||
@@ -192,17 +204,31 @@
|
|||||||
"create_new": "Create new environment",
|
"create_new": "Create new environment",
|
||||||
"created": "Environment created",
|
"created": "Environment created",
|
||||||
"deleted": "Environment deletion",
|
"deleted": "Environment deletion",
|
||||||
|
"duplicated": "Environment duplicated",
|
||||||
"edit": "Edit Environment",
|
"edit": "Edit Environment",
|
||||||
|
"global": "Global",
|
||||||
|
"empty_variables": "No variables",
|
||||||
|
"global_variables": "Global variables",
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
|
"list": "Environment variables",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "My Environments",
|
||||||
|
"name": "Name",
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||||
"new": "New Environment",
|
"new": "New Environment",
|
||||||
|
"no_active_environment": "No active environment",
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||||
|
"quick_peek": "Environment Quick Peek",
|
||||||
|
"replace_with_variable": "Replace with variable",
|
||||||
|
"scope": "Scope",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
|
"set": "Set environment",
|
||||||
|
"set_as_environment": "Set as environment",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Team Environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
|
"value": "Value",
|
||||||
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -226,6 +252,7 @@
|
|||||||
"no_duration": "No duration",
|
"no_duration": "No duration",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "No matches found",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "This page could not be found",
|
||||||
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Could not execute post-request script"
|
||||||
@@ -253,6 +280,10 @@
|
|||||||
"graphql": {
|
"graphql": {
|
||||||
"mutations": "Mutations",
|
"mutations": "Mutations",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
|
"switch_connection": "Switch connection",
|
||||||
|
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||||
|
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||||
|
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||||
"subscriptions": "Subscriptions"
|
"subscriptions": "Subscriptions"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
@@ -282,6 +313,30 @@
|
|||||||
"preview": "Hide Preview",
|
"preview": "Hide Preview",
|
||||||
"sidebar": "Collapse sidebar"
|
"sidebar": "Collapse sidebar"
|
||||||
},
|
},
|
||||||
|
"inspections": {
|
||||||
|
"title": "Inspector",
|
||||||
|
"description": "Inspect possible errors",
|
||||||
|
"environment": {
|
||||||
|
"add_environment": "Add to Environment",
|
||||||
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"401_error": "Please check your authentication credentials.",
|
||||||
|
"404_error": "Please check your request URL and method type.",
|
||||||
|
"network_error": "Please check your network connection.",
|
||||||
|
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||||
|
"default_error": "Please check your request."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"extension_not_installed": "Extension not installed.",
|
||||||
|
"extention_not_enabled": "Extension not enabled.",
|
||||||
|
"extention_enable_action": "Enable Browser Extension",
|
||||||
|
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
|
||||||
|
}
|
||||||
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "Import collections",
|
"collections": "Import collections",
|
||||||
"curl": "Import cURL",
|
"curl": "Import cURL",
|
||||||
@@ -418,8 +473,10 @@
|
|||||||
"payload": "Payload",
|
"payload": "Payload",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
|
"stop": "Stop",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
@@ -459,9 +516,9 @@
|
|||||||
"account_name_description": "This is your display name.",
|
"account_name_description": "This is your display name.",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"black_mode": "Black",
|
"black_mode": "Black",
|
||||||
|
"dark_mode": "Dark",
|
||||||
"change_font_size": "Change font size",
|
"change_font_size": "Change font size",
|
||||||
"choose_language": "Choose language",
|
"choose_language": "Choose language",
|
||||||
"dark_mode": "Dark",
|
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Delete account",
|
||||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||||
"expand_navigation": "Expand navigation",
|
"expand_navigation": "Expand navigation",
|
||||||
@@ -525,6 +582,10 @@
|
|||||||
"show_all": "Keyboard shortcuts",
|
"show_all": "Keyboard shortcuts",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
|
"others": {
|
||||||
|
"title": "Others",
|
||||||
|
"prettify": "Prettify Editor's Content"
|
||||||
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite people to Hoppscotch",
|
"invite": "Invite people to Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
@@ -545,6 +606,9 @@
|
|||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
|
"rename": "Rename Request",
|
||||||
|
"import_curl": "Import cURL",
|
||||||
|
"show_code": "Generate code snippet",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"next_method": "Select Next method",
|
"next_method": "Select Next method",
|
||||||
"post_method": "Select POST method",
|
"post_method": "Select POST method",
|
||||||
@@ -553,6 +617,7 @@
|
|||||||
"reset_request": "Reset Request",
|
"reset_request": "Reset Request",
|
||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
|
"save_request": "Save Request",
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -561,10 +626,10 @@
|
|||||||
"title": "Response"
|
"title": "Response"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Switch theme to black mode",
|
"black": "Switch theme to Black Mode",
|
||||||
"dark": "Switch theme to dark mode",
|
"dark": "Switch theme to Dark Mode",
|
||||||
"light": "Switch theme to light mode",
|
"light": "Switch theme to Light Mode",
|
||||||
"system": "Switch theme to system mode",
|
"system": "Switch theme to System Mode",
|
||||||
"title": "Theme"
|
"title": "Theme"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -582,6 +647,90 @@
|
|||||||
"log": "Log",
|
"log": "Log",
|
||||||
"url": "URL"
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
"event_type": "Event type",
|
"event_type": "Event type",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
@@ -639,8 +788,11 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "Authorization",
|
"authorization": "Authorization",
|
||||||
"body": "Body",
|
"body": "Body",
|
||||||
|
"close": "Close Tab",
|
||||||
|
"close_others": "Close other Tabs",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
|
"duplicate": "Duplicate Tab",
|
||||||
"environments": "Environments",
|
"environments": "Environments",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
|
|||||||
@@ -118,22 +118,22 @@
|
|||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "Koleksi dibuat",
|
"created": "Koleksi dibuat",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "Tidak dapat mengubah urutan koleksi dengan induk yang berbeda",
|
||||||
"edit": "Mengubah Koleksi",
|
"edit": "Mengubah Koleksi",
|
||||||
"invalid_name": "Berikan nama untuk Koleksi",
|
"invalid_name": "Berikan nama untuk Koleksi",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "Koleksi sudah berada di akar direktori",
|
||||||
"moved": "Moved Successfully",
|
"moved": "Berhasil Dipindahkan",
|
||||||
"my_collections": "Koleksi Saya",
|
"my_collections": "Koleksi Saya",
|
||||||
"name": "Koleksi Baru Saya",
|
"name": "Koleksi Baru Saya",
|
||||||
"name_length_insufficient": "Nama koleksi harus minimal 3 karakter",
|
"name_length_insufficient": "Nama koleksi harus minimal 3 karakter",
|
||||||
"new": "Koleksi baru",
|
"new": "Koleksi baru",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "Pembaruan Urutan Koleksi",
|
||||||
"renamed": "Koleksi berganti nama",
|
"renamed": "Koleksi berganti nama",
|
||||||
"request_in_use": "Permintaan sedang digunakan",
|
"request_in_use": "Permintaan sedang digunakan",
|
||||||
"save_as": "Simpan Sebagai",
|
"save_as": "Simpan Sebagai",
|
||||||
"select": "Pilih Koleksi",
|
"select": "Pilih Koleksi",
|
||||||
"select_location": "Pilih lokasi",
|
"select_location": "Pilih lokasi",
|
||||||
"select_team": "Pilih team",
|
"select_team": "Pilih tim",
|
||||||
"team_collections": "Koleksi Tim"
|
"team_collections": "Koleksi Tim"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"remove_team": "Apakah Anda yakin ingin menghapus tim ini?",
|
"remove_team": "Apakah Anda yakin ingin menghapus tim ini?",
|
||||||
"remove_telemetry": "Apakah Anda yakin ingin menyisih dari Telemetri?",
|
"remove_telemetry": "Apakah Anda yakin ingin menyisih dari Telemetri?",
|
||||||
"request_change": "Apakah Anda yakin ingin membuang permintaan saat ini, perubahan yang belum disimpan akan hilang.",
|
"request_change": "Apakah Anda yakin ingin membuang permintaan saat ini, perubahan yang belum disimpan akan hilang.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Apakah Anda ingin menyimpan perubahan yang dibuat di tab ini?",
|
||||||
"sync": "Apakah Anda ingin memulihkan ruang kerja Anda dari cloud? Ini akan membuang kemajuan lokal Anda."
|
"sync": "Apakah Anda ingin memulihkan ruang kerja Anda dari cloud? Ini akan membuang kemajuan lokal Anda."
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
@@ -180,8 +180,8 @@
|
|||||||
"profile": "Masuk untuk melihat profil Anda",
|
"profile": "Masuk untuk melihat profil Anda",
|
||||||
"protocols": "Protokol kosong",
|
"protocols": "Protokol kosong",
|
||||||
"schema": "Hubungkan ke endpoint GraphQL untuk melihat skema",
|
"schema": "Hubungkan ke endpoint GraphQL untuk melihat skema",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shortcodes": "Shortcodes kosong",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Langganan kosong",
|
||||||
"team_name": "Nama team kosong",
|
"team_name": "Nama team kosong",
|
||||||
"teams": "Kamu bukan di team manapun",
|
"teams": "Kamu bukan di team manapun",
|
||||||
"tests": "Tidak ada tes untuk permintaan ini"
|
"tests": "Tidak ada tes untuk permintaan ini"
|
||||||
@@ -189,19 +189,19 @@
|
|||||||
"environment": {
|
"environment": {
|
||||||
"add_to_global": "Tambahkan ke Global",
|
"add_to_global": "Tambahkan ke Global",
|
||||||
"added": "Tambahan Environment",
|
"added": "Tambahan Environment",
|
||||||
"create_new": "Membuat baru environment",
|
"create_new": "Membuat environment baru",
|
||||||
"created": "Environment dibuat",
|
"created": "Environment dibuat",
|
||||||
"deleted": "Environment dihapus",
|
"deleted": "Environment dihapus",
|
||||||
"edit": "Sunting Environment",
|
"edit": "Sunting Environment",
|
||||||
"invalid_name": "Tolong beri nama untuk environment",
|
"invalid_name": "Tolong beri nama untuk environment",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "Environment Saya",
|
||||||
"nested_overflow": "variabel environment bersarang dibatasi hingga 10 level",
|
"nested_overflow": "Variabel environment bersarang dibatasi hingga 10 level",
|
||||||
"new": "Environment Baru",
|
"new": "Environment Baru",
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"no_environment_description": "Tidak ada environment yang dipilih. Pilih apa yang harus dilakukan dengan variabel berikut.",
|
"no_environment_description": "Tidak ada environment yang dipilih. Pilih apa yang harus dilakukan dengan variabel berikut.",
|
||||||
"select": "Pilih environment",
|
"select": "Pilih environment",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Environment Tim",
|
||||||
"title": "Environments",
|
"title": "Environment",
|
||||||
"updated": "Environment diperbarui",
|
"updated": "Environment diperbarui",
|
||||||
"variable_list": "Daftar Variable"
|
"variable_list": "Daftar Variable"
|
||||||
},
|
},
|
||||||
@@ -210,8 +210,8 @@
|
|||||||
"check_console_details": "Periksa console log untuk detailnya.",
|
"check_console_details": "Periksa console log untuk detailnya.",
|
||||||
"curl_invalid_format": "cURL tidak diformat dengan benar",
|
"curl_invalid_format": "cURL tidak diformat dengan benar",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Danger zone",
|
||||||
"delete_account": "Your account is currently an owner in these teams:",
|
"delete_account": "Akun Anda saat ini merupakan pemilik dalam tim-tim ini:",
|
||||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
"delete_account_description": "Anda harus menghapus diri Anda dari tim-tim ini, mentransfer kepemilikan, atau menghapus tim-tim ini sebelum Anda dapat menghapus akun Anda.",
|
||||||
"empty_req_name": "Nama Permintaan Kosong",
|
"empty_req_name": "Nama Permintaan Kosong",
|
||||||
"f12_details": "(F12 untuk detailnya)",
|
"f12_details": "(F12 untuk detailnya)",
|
||||||
"gql_prettify_invalid_query": "Tidak dapat prettify kueri yang tidak valid, menyelesaikan kesalahan sintaksis kueri, dan coba lagi",
|
"gql_prettify_invalid_query": "Tidak dapat prettify kueri yang tidak valid, menyelesaikan kesalahan sintaksis kueri, dan coba lagi",
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
"from_json_description": "Impor dari Hoppscotch berkas koleksi",
|
"from_json_description": "Impor dari Hoppscotch berkas koleksi",
|
||||||
"from_my_collections": "Impor dari Koleksi Saya",
|
"from_my_collections": "Impor dari Koleksi Saya",
|
||||||
"from_my_collections_description": "Impor dari Berkas Koleksi Saya",
|
"from_my_collections_description": "Impor dari Berkas Koleksi Saya",
|
||||||
"from_openapi": "Import dari OpenAPI",
|
"from_openapi": "Impor dari OpenAPI",
|
||||||
"from_openapi_description": "Impor dari OpenAPI syarat berkas (YML/JSON)",
|
"from_openapi_description": "Impor dari OpenAPI syarat berkas (YML/JSON)",
|
||||||
"from_postman": "Impor dari Postman",
|
"from_postman": "Impor dari Postman",
|
||||||
"from_postman_description": "Impor dari Koleksi Postman",
|
"from_postman_description": "Impor dari Koleksi Postman",
|
||||||
@@ -316,23 +316,23 @@
|
|||||||
"zen_mode": "Zen mode"
|
"zen_mode": "Zen mode"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "Anda memiliki perubahan yang belum disimpan",
|
||||||
"collections": "Koleksi",
|
"collections": "Koleksi",
|
||||||
"confirm": "Mengonfirmasi",
|
"confirm": "Mengonfirmasi",
|
||||||
"edit_request": "Edit Request",
|
"edit_request": "Edit Request",
|
||||||
"import_export": "Impor / Ekspor"
|
"import_export": "Impor / Ekspor"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
"already_subscribed": "You are already subscribed to this topic.",
|
"already_subscribed": "Anda sudah berlangganan topik ini.",
|
||||||
"clean_session": "Clean Session",
|
"clean_session": "Sesi Bersih",
|
||||||
"clear_input": "Clear input",
|
"clear_input": "Hapus input",
|
||||||
"clear_input_on_send": "Clear input on send",
|
"clear_input_on_send": "Hapus input saat mengirim",
|
||||||
"client_id": "Client ID",
|
"client_id": "Client ID",
|
||||||
"color": "Pick a color",
|
"color": "Pilih warna",
|
||||||
"communication": "Komunikasi",
|
"communication": "Komunikasi",
|
||||||
"connection_config": "Connection Config",
|
"connection_config": "Konfigurasi Koneksi",
|
||||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
"connection_not_authorized": "Koneksi MQTT ini tidak menggunakan otentikasi",
|
||||||
"invalid_topic": "Please provide a topic for the subscription",
|
"invalid_topic": "Harap berikan topik untuk langganan",
|
||||||
"keep_alive": "Keep Alive",
|
"keep_alive": "Keep Alive",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
"lw_message": "Last-Will Message",
|
"lw_message": "Last-Will Message",
|
||||||
@@ -340,8 +340,8 @@
|
|||||||
"lw_retain": "Last-Will Retain",
|
"lw_retain": "Last-Will Retain",
|
||||||
"lw_topic": "Last-Will Topic",
|
"lw_topic": "Last-Will Topic",
|
||||||
"message": "Pesan",
|
"message": "Pesan",
|
||||||
"new": "New Subscription",
|
"new": "Langganan Baru",
|
||||||
"not_connected": "Please start a MQTT connection first.",
|
"not_connected": "Mulai koneksi MQTT terlebih dahulu",
|
||||||
"publish": "Menerbitkan",
|
"publish": "Menerbitkan",
|
||||||
"qos": "QoS",
|
"qos": "QoS",
|
||||||
"ssl": "SSL",
|
"ssl": "SSL",
|
||||||
@@ -396,19 +396,19 @@
|
|||||||
"text": "Text"
|
"text": "Text"
|
||||||
},
|
},
|
||||||
"copy_link": "Salin tautan",
|
"copy_link": "Salin tautan",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Tidak dapat mengubah urutan permintaan dari koleksi yang berbeda",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "Durasi",
|
"duration": "Durasi",
|
||||||
"enter_curl": "Masukkan cURL",
|
"enter_curl": "Masukkan cURL",
|
||||||
"generate_code": "Generate code",
|
"generate_code": "Hasilkan kode",
|
||||||
"generated_code": "Generated code",
|
"generated_code": "Hasilkan kode",
|
||||||
"header_list": "Daftar Header",
|
"header_list": "Daftar Header",
|
||||||
"invalid_name": "Harap berikan nama untuk request",
|
"invalid_name": "Harap berikan nama untuk request",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"moved": "Request moved",
|
"moved": "Request moved",
|
||||||
"name": "Request nama",
|
"name": "Request nama",
|
||||||
"new": "Request baru",
|
"new": "Request baru",
|
||||||
"order_changed": "Request Order Updated",
|
"order_changed": "Urutan Request Diperbarui",
|
||||||
"override": "Membatalkan",
|
"override": "Membatalkan",
|
||||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
||||||
"overriden": "Diganti",
|
"overriden": "Diganti",
|
||||||
@@ -453,7 +453,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accent_color": "Accent color",
|
"accent_color": "Accent color",
|
||||||
"account": "Akun",
|
"account": "Akun",
|
||||||
"account_deleted": "Your account has been deleted",
|
"account_deleted": "Akun Anda telah dihapus",
|
||||||
"account_description": "Sesuaikan pengaturan akun Anda.",
|
"account_description": "Sesuaikan pengaturan akun Anda.",
|
||||||
"account_email_description": "Alamat surel utama Anda.",
|
"account_email_description": "Alamat surel utama Anda.",
|
||||||
"account_name_description": "Ini adalah nama tampilan Anda.",
|
"account_name_description": "Ini adalah nama tampilan Anda.",
|
||||||
@@ -609,7 +609,7 @@
|
|||||||
"file_imported": "File diimpor",
|
"file_imported": "File diimpor",
|
||||||
"finished_in": "Selesai dalam {duration} ms",
|
"finished_in": "Selesai dalam {duration} ms",
|
||||||
"history_deleted": "Riwayat dihapus",
|
"history_deleted": "Riwayat dihapus",
|
||||||
"linewrap": "Wrap lines",
|
"linewrap": "Bungkus baris",
|
||||||
"loading": "Memuat...",
|
"loading": "Memuat...",
|
||||||
"message_received": "Pesan: {message} tiba di topik: {topic}",
|
"message_received": "Pesan: {message} tiba di topik: {topic}",
|
||||||
"mqtt_subscription_failed": "Terjadi masalah saat berlangganan topik: {topic}",
|
"mqtt_subscription_failed": "Terjadi masalah saat berlangganan topik: {topic}",
|
||||||
@@ -666,7 +666,7 @@
|
|||||||
"email_do_not_match": "Surel tidak cocok dengan detail akun Anda. Hubungi pemilik tim Anda.",
|
"email_do_not_match": "Surel tidak cocok dengan detail akun Anda. Hubungi pemilik tim Anda.",
|
||||||
"exit": "Keluar dari Tim",
|
"exit": "Keluar dari Tim",
|
||||||
"exit_disabled": "Hanya pemilik yang tidak dapat keluar dari tim",
|
"exit_disabled": "Hanya pemilik yang tidak dapat keluar dari tim",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "ID koleksi tidak valid",
|
||||||
"invalid_email_format": "Format surel tidak valid",
|
"invalid_email_format": "Format surel tidak valid",
|
||||||
"invalid_id": "ID tim tidak valid. Hubungi pemilik tim Anda.",
|
"invalid_id": "ID tim tidak valid. Hubungi pemilik tim Anda.",
|
||||||
"invalid_invite_link": "Tautan undangan tidak valid",
|
"invalid_invite_link": "Tautan undangan tidak valid",
|
||||||
@@ -690,7 +690,7 @@
|
|||||||
"member_removed": "Pengguna dihapus",
|
"member_removed": "Pengguna dihapus",
|
||||||
"member_role_updated": "Peran pengguna diperbarui",
|
"member_role_updated": "Peran pengguna diperbarui",
|
||||||
"members": "Anggota",
|
"members": "Anggota",
|
||||||
"more_members": "+{count} more",
|
"more_members": "+{count} lebih",
|
||||||
"name_length_insufficient": "Nama tim harus setidaknya 6 karakter",
|
"name_length_insufficient": "Nama tim harus setidaknya 6 karakter",
|
||||||
"name_updated": "Nama tim diperbarui",
|
"name_updated": "Nama tim diperbarui",
|
||||||
"new": "Tim Baru",
|
"new": "Tim Baru",
|
||||||
@@ -698,13 +698,13 @@
|
|||||||
"new_name": "Tim baru saya",
|
"new_name": "Tim baru saya",
|
||||||
"no_access": "Anda tidak memiliki akses edit ke collections ini",
|
"no_access": "Anda tidak memiliki akses edit ke collections ini",
|
||||||
"no_invite_found": "Undangan tidak ditemukan. Hubungi pemilik tim Anda.",
|
"no_invite_found": "Undangan tidak ditemukan. Hubungi pemilik tim Anda.",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "Request tidak ditemukan.",
|
||||||
"not_found": "Tim tidak ditemukan. Hubungi pemilik tim Anda.",
|
"not_found": "Tim tidak ditemukan. Hubungi pemilik tim Anda.",
|
||||||
"not_valid_viewer": "Anda bukan penonton yang valid. Hubungi pemilik tim Anda.",
|
"not_valid_viewer": "Anda bukan penonton yang valid. Hubungi pemilik tim Anda.",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "Tidak dapat memindahkan koleksi ke dalam koleksi anak",
|
||||||
"pending_invites": "Undangan tertunda",
|
"pending_invites": "Undangan tertunda",
|
||||||
"permissions": "Izin",
|
"permissions": "Izin",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "Sama tujuan dan destinasi",
|
||||||
"saved": "Tim disimpan",
|
"saved": "Tim disimpan",
|
||||||
"select_a_team": "Pilih tim",
|
"select_a_team": "Pilih tim",
|
||||||
"title": "tim",
|
"title": "tim",
|
||||||
@@ -712,9 +712,9 @@
|
|||||||
"we_sent_invite_link_description": "Minta semua undangan untuk memeriksa kotak masuk mereka. Klik tautan untuk bergabung dengan tim."
|
"we_sent_invite_link_description": "Minta semua undangan untuk memeriksa kotak masuk mereka. Klik tautan untuk bergabung dengan tim."
|
||||||
},
|
},
|
||||||
"team_environment": {
|
"team_environment": {
|
||||||
"deleted": "Environment Deleted",
|
"deleted": "Environment dihapus",
|
||||||
"duplicate": "Environment Duplicated",
|
"duplicate": "Environment diduplikasi",
|
||||||
"not_found": "Environment not found."
|
"not_found": "Environment tidak ditemukan."
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"failed": "Tes gagal",
|
"failed": "Tes gagal",
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "Beralih workspace",
|
||||||
"personal": "My Workspace",
|
"personal": "Workspace Saya",
|
||||||
"team": "Team Workspace",
|
"team": "Workspace Tim",
|
||||||
"title": "Workspaces"
|
"title": "Workspaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,50 @@
|
|||||||
{
|
{
|
||||||
"action": {
|
"action": {
|
||||||
"autoscroll": "Autoscroll",
|
"autoscroll": "Автоскрол",
|
||||||
"cancel": "Отменить",
|
"cancel": "Отменить",
|
||||||
"choose_file": "Выберите файл",
|
"choose_file": "Выберите файл",
|
||||||
"clear": "Очистить",
|
"clear": "Очистить",
|
||||||
"clear_all": "Очистить все",
|
"clear_all": "Очистить все",
|
||||||
"close": "Close",
|
"clear_history": "Очистить всю историю",
|
||||||
|
"close": "Закрыть",
|
||||||
"connect": "Подключиться",
|
"connect": "Подключиться",
|
||||||
"connecting": "Connecting",
|
"connecting": "Соединение...",
|
||||||
"copy": "Скопировать",
|
"copy": "Скопировать",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"disconnect": "Отключиться",
|
"disconnect": "Отключиться",
|
||||||
"dismiss": "Скрыть",
|
"dismiss": "Скрыть",
|
||||||
"dont_save": "Don't save",
|
"dont_save": "Не сохранять",
|
||||||
"download_file": "Скачать файл",
|
"download_file": "Скачать файл",
|
||||||
"drag_to_reorder": "Drag to reorder",
|
"drag_to_reorder": "Перетягивайте для сортировки",
|
||||||
"duplicate": "Дублировать",
|
"duplicate": "Дублировать",
|
||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
"filter": "Filter",
|
"filter": "Фильтр",
|
||||||
"go_back": "Вернуться",
|
"go_back": "Вернуться",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "Вперёд",
|
||||||
"group_by": "Group by",
|
"group_by": "Сгруппировать по",
|
||||||
"label": "Название",
|
"label": "Название",
|
||||||
"learn_more": "Узнать больше",
|
"learn_more": "Узнать больше",
|
||||||
"less": "Less",
|
"less": "Меньше",
|
||||||
"more": "Больше",
|
"more": "Больше",
|
||||||
"new": "Создать новый",
|
"new": "Создать новый",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Открыть пространство",
|
||||||
"paste": "Paste",
|
"paste": "Вставить",
|
||||||
"prettify": "Форматировать",
|
"prettify": "Форматировать",
|
||||||
|
"rename": "Переименовать",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"restore": "Восстановить",
|
"restore": "Восстановить",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"scroll_to_bottom": "Scroll to bottom",
|
"scroll_to_bottom": "Вниз",
|
||||||
"scroll_to_top": "Scroll to top",
|
"scroll_to_top": "Вверх",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"send": "Отправить",
|
"send": "Отправить",
|
||||||
"start": "Начать",
|
"start": "Начать",
|
||||||
"starting": "Starting",
|
"starting": "Запускаю",
|
||||||
"stop": "Стоп",
|
"stop": "Стоп",
|
||||||
"to_close": "to close",
|
"to_close": "что бы закрыть",
|
||||||
"to_navigate": "to navigate",
|
"to_navigate": "для навигации",
|
||||||
"to_select": "to select",
|
"to_select": "выборать",
|
||||||
"turn_off": "Выключить",
|
"turn_off": "Выключить",
|
||||||
"turn_on": "Включить",
|
"turn_on": "Включить",
|
||||||
"undo": "Отменить",
|
"undo": "Отменить",
|
||||||
@@ -56,9 +58,9 @@
|
|||||||
"chat_with_us": "Связаться с нами",
|
"chat_with_us": "Связаться с нами",
|
||||||
"contact_us": "Свяжитесь с нами",
|
"contact_us": "Свяжитесь с нами",
|
||||||
"copy": "Копировать",
|
"copy": "Копировать",
|
||||||
"copy_user_id": "Copy User Auth Token",
|
"copy_user_id": "Копировать токен пользователя",
|
||||||
"developer_option": "Developer options",
|
"developer_option": "Настройки разработчика",
|
||||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
"developer_option_description": "Инструмент разработчика помогает обслуживить и развивить Hoppscotch",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"documentation": "Документация",
|
"documentation": "Документация",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
@@ -67,11 +69,13 @@
|
|||||||
"invite": "Пригласить",
|
"invite": "Пригласить",
|
||||||
"invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.",
|
"invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.",
|
||||||
"invite_your_friends": "Пригласить своих друзей",
|
"invite_your_friends": "Пригласить своих друзей",
|
||||||
|
"social_links": "Социальные сети",
|
||||||
|
"social_description": "Подписывайся на наши соц. сети и оставайся всегда в курсе последних новостей, обновлений и релизов.",
|
||||||
"join_discord_community": "Присоединяйтесь к нашему сообществу Discord",
|
"join_discord_community": "Присоединяйтесь к нашему сообществу Discord",
|
||||||
"keyboard_shortcuts": "Горячие клавиши",
|
"keyboard_shortcuts": "Горячие клавиши",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
"new_version_found": "Найдена новая версия. Перезагрузите для обновления.",
|
"new_version_found": "Найдена новая версия. Перезагрузите для обновления.",
|
||||||
"options": "Options",
|
"options": "Настройки",
|
||||||
"proxy_privacy_policy": "Политика конфиденциальности прокси",
|
"proxy_privacy_policy": "Политика конфиденциальности прокси",
|
||||||
"reload": "Перезагрузить",
|
"reload": "Перезагрузить",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
"shortcuts": "Ярлыки",
|
"shortcuts": "Ярлыки",
|
||||||
"spotlight": "Прожектор",
|
"spotlight": "Прожектор",
|
||||||
"status": "Статус",
|
"status": "Статус",
|
||||||
"status_description": "Check the status of the website",
|
"status_description": "Проверить состояние сайта",
|
||||||
"terms_and_privacy": "Условия и конфиденциальность",
|
"terms_and_privacy": "Условия и конфиденциальность",
|
||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"type_a_command_search": "Введите команду или выполните поиск…",
|
"type_a_command_search": "Введите команду или выполните поиск…",
|
||||||
@@ -93,7 +97,7 @@
|
|||||||
"continue_with_email": "Продолжить с электронной почтой",
|
"continue_with_email": "Продолжить с электронной почтой",
|
||||||
"continue_with_github": "Продолжить с GitHub",
|
"continue_with_github": "Продолжить с GitHub",
|
||||||
"continue_with_google": "Продолжить с Google",
|
"continue_with_google": "Продолжить с Google",
|
||||||
"continue_with_microsoft": "Continue with Microsoft",
|
"continue_with_microsoft": "Продолжить с Microsoft",
|
||||||
"email": "Электронное письмо",
|
"email": "Электронное письмо",
|
||||||
"logged_out": "Вышли из",
|
"logged_out": "Вышли из",
|
||||||
"login": "Авторизоваться",
|
"login": "Авторизоваться",
|
||||||
@@ -118,19 +122,20 @@
|
|||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "Коллекция создана",
|
"created": "Коллекция создана",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией",
|
||||||
"edit": "Редактировать коллекцию",
|
"edit": "Редактировать коллекцию",
|
||||||
"invalid_name": "Укажите допустимое название коллекции",
|
"invalid_name": "Укажите допустимое название коллекции",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "Коллекция уже в корне",
|
||||||
"moved": "Moved Successfully",
|
"moved": "Перемещено успешно",
|
||||||
"my_collections": "Мои коллекции",
|
"my_collections": "Мои коллекции",
|
||||||
"name": "Новая коллекция",
|
"name": "Новая коллекция",
|
||||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
"name_length_insufficient": "Имя коллекции должно иметь 3 или более символов",
|
||||||
"new": "Создать коллекцию",
|
"new": "Создать коллекцию",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "Порядок коллекции обновлён",
|
||||||
"renamed": "Коллекция переименована",
|
"renamed": "Коллекция переименована",
|
||||||
"request_in_use": "Запрос обрабатывается",
|
"request_in_use": "Запрос обрабатывается",
|
||||||
"save_as": "Сохранить как",
|
"save_as": "Сохранить как",
|
||||||
|
"save_to_collection": "Сохранить в коллекцию",
|
||||||
"select": "Выбрать коллекцию",
|
"select": "Выбрать коллекцию",
|
||||||
"select_location": "Выберите местоположение",
|
"select_location": "Выберите местоположение",
|
||||||
"select_team": "Выберите команду",
|
"select_team": "Выберите команду",
|
||||||
@@ -146,10 +151,17 @@
|
|||||||
"remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
|
"remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
|
||||||
"remove_team": "Вы уверены, что хотите удалить эту команду?",
|
"remove_team": "Вы уверены, что хотите удалить эту команду?",
|
||||||
"remove_telemetry": "Вы действительно хотите отказаться от телеметрии?",
|
"remove_telemetry": "Вы действительно хотите отказаться от телеметрии?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Вы уверены что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Вы хотите сохранить изменения в этой вкладке?",
|
||||||
|
"close_unsaved_tab": "Вы уверены что хотите закрыть эту вкладку?",
|
||||||
|
"close_unsaved_tabs": "ВЫ уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
|
||||||
"sync": "Вы уверены, что хотите синхронизировать это рабочее пространство?"
|
"sync": "Вы уверены, что хотите синхронизировать это рабочее пространство?"
|
||||||
},
|
},
|
||||||
|
"context_menu": {
|
||||||
|
"set_environment_variable": "Назначить как переменную",
|
||||||
|
"add_parameters": "Добавить в параметры",
|
||||||
|
"open_request_in_new_tab": "Открыть в новой вкладке"
|
||||||
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Заголовок {count}",
|
"header": "Заголовок {count}",
|
||||||
"message": "Тело {count}",
|
"message": "Тело {count}",
|
||||||
@@ -180,83 +192,102 @@
|
|||||||
"profile": "Войдите, чтобы просмотреть свой профиль",
|
"profile": "Войдите, чтобы просмотреть свой профиль",
|
||||||
"protocols": "Протоколы пустые",
|
"protocols": "Протоколы пустые",
|
||||||
"schema": "Подключиться к конечной точке GraphQL",
|
"schema": "Подключиться к конечной точке GraphQL",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shortcodes": "Нет коротких ссылок",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Нет подписок",
|
||||||
"team_name": "Название команды пусто",
|
"team_name": "Название команды пусто",
|
||||||
"teams": "Команды пустые",
|
"teams": "Команды пустые",
|
||||||
"tests": "Для этого запроса нет тестов"
|
"tests": "Для этого запроса нет тестов"
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"add_to_global": "Add to Global",
|
"add_to_global": "Добавить в глобальное окружение",
|
||||||
"added": "Environment addition",
|
"added": "Окружение добавлено",
|
||||||
"create_new": "Создать новую среду",
|
"create_new": "Создать новое окружение",
|
||||||
"created": "Environment created",
|
"created": "Окружение создано",
|
||||||
"deleted": "Environment deletion",
|
"deleted": "Окружение удалено",
|
||||||
"edit": "Редактировать среду",
|
"duplicated": "Окружение скопировано",
|
||||||
"invalid_name": "Укажите допустимое имя для среды",
|
"global": "Глобальное окружение",
|
||||||
"my_environments": "My Environments",
|
"empty_variables": "Нет переменных",
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"global_variables": "Глобальные переменные",
|
||||||
|
"edit": "Редактировать окружение",
|
||||||
|
"invalid_name": "Укажите допустимое имя для окружения",
|
||||||
|
"list": "Переменные окружения",
|
||||||
|
"my_environments": "Мои окружения",
|
||||||
|
"name": "Название",
|
||||||
|
"nested_overflow": "максимальный уровень вложения переменных окружения - 10",
|
||||||
"new": "Новая среда",
|
"new": "Новая среда",
|
||||||
"no_environment": "Нет окружающей среды",
|
"no_active_environment": "Нет активных окружений",
|
||||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
"no_environment": "Нет окружения",
|
||||||
|
"no_environment_description": "Не выбрано окружение, выберите что делать с переменными.",
|
||||||
|
"quick_peek": "Быстрый просмотр окружения",
|
||||||
|
"replace_with_variable": "Заменить переменной",
|
||||||
|
"scope": "Scope",
|
||||||
"select": "Выберите среду",
|
"select": "Выберите среду",
|
||||||
"team_environments": "Team Environments",
|
"set": "Выбрать окружение",
|
||||||
"title": "Среды",
|
"set_as_environment": "Установить как окружение",
|
||||||
"updated": "Environment updation",
|
"team_environments": "Окружения команды",
|
||||||
|
"title": "Окружения",
|
||||||
|
"updated": "Окружение обновлено",
|
||||||
|
"value": "Значение",
|
||||||
|
"variable": "Переменная",
|
||||||
"variable_list": "Список переменных"
|
"variable_list": "Список переменных"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
|
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
|
||||||
"check_console_details": "Подробности смотрите в журнале консоли.",
|
"check_console_details": "Подробности смотрите в журнале консоли.",
|
||||||
"curl_invalid_format": "cURL неправильно отформатирован",
|
"curl_invalid_format": "cURL неправильно отформатирован",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Опасная зона",
|
||||||
"delete_account": "Your account is currently an owner in these teams:",
|
"delete_account": "Вы являетесь владельцем этой команды:",
|
||||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
"delete_account_description": "Прежде чем удалить аккаунт вам необходимо либо назначить владельцом другого пользователя, либо удалить команды в которых вы являетесь владельцем.",
|
||||||
"empty_req_name": "Пустое имя запроса",
|
"empty_req_name": "Пустое имя запроса",
|
||||||
"f12_details": "(F12 для подробностей)",
|
"f12_details": "(F12 для подробностей)",
|
||||||
"gql_prettify_invalid_query": "Не удалось определить недопустимый запрос, устранить синтаксические ошибки запроса и повторить попытку.",
|
"gql_prettify_invalid_query": "Не удалось определить недопустимый запрос, устранить синтаксические ошибки запроса и повторить попытку.",
|
||||||
"incomplete_config_urls": "Incomplete configuration URLs",
|
"incomplete_config_urls": "Не заполнены URL конфигурации",
|
||||||
"incorrect_email": "Incorrect email",
|
"incorrect_email": "Не корректный Email",
|
||||||
"invalid_link": "Invalid link",
|
"invalid_link": "Не корректная ссылка",
|
||||||
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
|
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
|
||||||
"json_parsing_failed": "Invalid JSON",
|
"json_parsing_failed": "Не корректный JSON",
|
||||||
"json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
|
"json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
|
||||||
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
|
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
|
||||||
"network_fail": "Не удалось отправить запрос",
|
"network_fail": "Не удалось отправить запрос",
|
||||||
"no_duration": "Без продолжительности",
|
"no_duration": "Без продолжительности",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "Совпадения не найдены",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "Эта страница не найдена",
|
||||||
|
"proxy_error": "Ошибка прокси",
|
||||||
"script_fail": "Не удалось выполнить сценарий предварительного запроса",
|
"script_fail": "Не удалось выполнить сценарий предварительного запроса",
|
||||||
"something_went_wrong": "Что-то пошло не так",
|
"something_went_wrong": "Что-то пошло не так",
|
||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Не удалось выполнить тестирование запроса"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"as_json": "Экспорт как JSON",
|
"as_json": "Экспорт как JSON",
|
||||||
"create_secret_gist": "Создать секретный Gist",
|
"create_secret_gist": "Создать секретный Gist",
|
||||||
"gist_created": "Gist создан",
|
"gist_created": "Gist создан",
|
||||||
"require_github": "Войдите через GitHub, чтобы создать секретную суть",
|
"require_github": "Войдите через GitHub, чтобы создать секретную суть",
|
||||||
"title": "Export"
|
"title": "Экспорт"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"all": "All",
|
"all": "Все",
|
||||||
"none": "None",
|
"none": "Не указано",
|
||||||
"starred": "Starred"
|
"starred": "Отмечено"
|
||||||
},
|
},
|
||||||
"folder": {
|
"folder": {
|
||||||
"created": "Папка создана",
|
"created": "Папка создана",
|
||||||
"edit": "Редактировать папку",
|
"edit": "Редактировать папку",
|
||||||
"invalid_name": "Укажите имя для папки",
|
"invalid_name": "Укажите имя для папки",
|
||||||
"name_length_insufficient": "Folder name should be at least 3 characters long",
|
"name_length_insufficient": "Имя папки должно содержать 3 или более символов",
|
||||||
"new": "Новая папка",
|
"new": "Новая папка",
|
||||||
"renamed": "Папка переименована"
|
"renamed": "Папка переименована"
|
||||||
},
|
},
|
||||||
"graphql": {
|
"graphql": {
|
||||||
"mutations": "Мутации",
|
"mutations": "Мутации",
|
||||||
"schema": "Схема",
|
"schema": "Схема",
|
||||||
"subscriptions": "Подписки"
|
"subscriptions": "Подписки",
|
||||||
|
"switch_connection": "Изменить соединение",
|
||||||
|
"connection_switch_url": "Вы присоединились к GraphQL, URL соединения",
|
||||||
|
"connection_switch_new_url": "Смена вкладки разорвёт текущее GraphQL соединение. Новый URL соединения будет",
|
||||||
|
"connection_switch_confirm": "Вы желаете соединиться с последним GraphQL сервером?"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"time": "Time",
|
"time": "Время",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
@@ -273,11 +304,11 @@
|
|||||||
"post_request_tests": "Сценарии тестирования написаны на JavaScript и запускаются после получения ответа.",
|
"post_request_tests": "Сценарии тестирования написаны на JavaScript и запускаются после получения ответа.",
|
||||||
"pre_request_script": "Скрипты предварительного запроса написаны на JavaScript и запускаются перед отправкой запроса.",
|
"pre_request_script": "Скрипты предварительного запроса написаны на JavaScript и запускаются перед отправкой запроса.",
|
||||||
"script_fail": "Похоже, в скрипте предварительного запроса есть сбой. Проверьте ошибку ниже и исправьте скрипт соответствующим образом.",
|
"script_fail": "Похоже, в скрипте предварительного запроса есть сбой. Проверьте ошибку ниже и исправьте скрипт соответствующим образом.",
|
||||||
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
"test_script_fail": "Похоже, что скрипт тестирования содержит ошибку. Пожалуйста исправьте её и попробуйте снова",
|
||||||
"tests": "Напишите тестовый сценарий для автоматизации отладки."
|
"tests": "Напишите тестовый сценарий для автоматизации отладки."
|
||||||
},
|
},
|
||||||
"hide": {
|
"hide": {
|
||||||
"collection": "Collapse Collection Panel",
|
"collection": "Свернуть панель соединения",
|
||||||
"more": "Скрыть больше",
|
"more": "Скрыть больше",
|
||||||
"preview": "Скрыть предварительный просмотр",
|
"preview": "Скрыть предварительный просмотр",
|
||||||
"sidebar": "Скрыть боковую панель"
|
"sidebar": "Скрыть боковую панель"
|
||||||
@@ -287,61 +318,85 @@
|
|||||||
"curl": "Импортировать cURL",
|
"curl": "Импортировать cURL",
|
||||||
"failed": "Ошибка импорта",
|
"failed": "Ошибка импорта",
|
||||||
"from_gist": "Импорт из Gist",
|
"from_gist": "Импорт из Gist",
|
||||||
"from_gist_description": "Import from Gist URL",
|
"from_gist_description": "Импортировать через Gist URL",
|
||||||
"from_insomnia": "Import from Insomnia",
|
"from_insomnia": "Импортировать с Insomnia",
|
||||||
"from_insomnia_description": "Import from Insomnia collection",
|
"from_insomnia_description": "Импортировать из коллекции Insomnia",
|
||||||
"from_json": "Import from Hoppscotch",
|
"from_json": "Импортировать из Hoppscotch",
|
||||||
"from_json_description": "Import from Hoppscotch collection file",
|
"from_json_description": "Импортировать из файла коллекции Hoppscotch",
|
||||||
"from_my_collections": "Импортировать из моих коллекций",
|
"from_my_collections": "Импортировать из моих коллекций",
|
||||||
"from_my_collections_description": "Import from My Collections file",
|
"from_my_collections_description": "Импортировать коллекции из моего файла",
|
||||||
"from_openapi": "Import from OpenAPI",
|
"from_openapi": "Импортировать из OpenAPI",
|
||||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
"from_openapi_description": "Импортировать из OpenAPI файла описания API (YML/JSON)",
|
||||||
"from_postman": "Import from Postman",
|
"from_postman": "Импортировать из Postman",
|
||||||
"from_postman_description": "Import from Postman collection",
|
"from_postman_description": "Импортировать из коллекции Postman",
|
||||||
"from_url": "Import from URL",
|
"from_url": "Импортировать из URL",
|
||||||
"gist_url": "Введите URL-адрес Gist",
|
"gist_url": "Введите URL-адрес Gist",
|
||||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
"import_from_url_invalid_fetch": "Не удалить получить данные по этому URL",
|
||||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
"import_from_url_invalid_file_format": "Ошибка при импорте коллекций",
|
||||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
"import_from_url_invalid_type": "Неподдерживаемый тип. Поддерживаемые типы: 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||||
"import_from_url_success": "Collections Imported",
|
"import_from_url_success": "Коллекция импортирована",
|
||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Импортировать из коллекции Hoppscotch",
|
||||||
"title": "Импортировать"
|
"title": "Импортировать"
|
||||||
},
|
},
|
||||||
|
"inspections": {
|
||||||
|
"title": "Инспектор",
|
||||||
|
"description": "Помогает обноружить возможные проблемы",
|
||||||
|
"environment": {
|
||||||
|
"add_environment": "Добавить в окружение",
|
||||||
|
"not_found": "Переменная окружения “{environment}” не найдена."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"cookie": "Браузерная версия Hoppscotch не может использовать Cookie Header. Пока мы работаем над созданием десктоп версии Hoppscotch, пожалуйста используйте Authorization Header."
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"401_error": "Пожалуйста проверьте ваши параметры авторизации",
|
||||||
|
"404_error": "Пожалуйста проверьте URL и Метод вашего запроса",
|
||||||
|
"network_error": "Пожалуйста проверьте соединение",
|
||||||
|
"cors_error": "Пожалуйста проверьте вашу Cross-Origin Resource Sharing настройку сервера.",
|
||||||
|
"default_error": "Проверьте ваш запрос."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"extension_not_installed": "Расширение не установлено",
|
||||||
|
"extention_not_enabled": "Расширение не включено",
|
||||||
|
"extention_enable_action": "Включить расширение браузера",
|
||||||
|
"extension_unknown_origin": "Убедитесь что вы добавили адрес сервера в Hoppscotch расширение."
|
||||||
|
}
|
||||||
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"collapse_collection": "Collapse or Expand Collections",
|
"collapse_collection": "Свернуть или развернуть коллекции",
|
||||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
"collapse_sidebar": "Свернуть или развернуть боковую панель",
|
||||||
"column": "Вертикальное оформление",
|
"column": "Вертикальная развёртка",
|
||||||
"name": "Layout",
|
"name": "Развёртка",
|
||||||
"row": "Горизонтальное оформление",
|
"row": "Горизонтальная развертка",
|
||||||
"zen_mode": "Спокойный режим"
|
"zen_mode": "Спокойный режим"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "У вас есть не сохранённые изменения",
|
||||||
"collections": "Коллекции",
|
"collections": "Коллекции",
|
||||||
"confirm": "Подтверждать",
|
"confirm": "Подтверждать",
|
||||||
"edit_request": "Изменить запрос",
|
"edit_request": "Изменить запрос",
|
||||||
"import_export": "Импорт Экспорт"
|
"import_export": "Импорт Экспорт"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
"already_subscribed": "You are already subscribed to this topic.",
|
"already_subscribed": "Вы уже подписаны на этот топик",
|
||||||
"clean_session": "Clean Session",
|
"clean_session": "Очистить сессию",
|
||||||
"clear_input": "Clear input",
|
"clear_input": "Очистить ввод",
|
||||||
"clear_input_on_send": "Clear input on send",
|
"clear_input_on_send": "Очистить ввод перед отправкой",
|
||||||
"client_id": "Client ID",
|
"client_id": "Client ID",
|
||||||
"color": "Pick a color",
|
"color": "Выбрать цвет",
|
||||||
"communication": "Коммуникация",
|
"communication": "Коммуникация",
|
||||||
"connection_config": "Connection Config",
|
"connection_config": "Конфигурация соединения",
|
||||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
"connection_not_authorized": "Это соединение MQTT не использует какую-либо авторизацию.",
|
||||||
"invalid_topic": "Please provide a topic for the subscription",
|
"invalid_topic": "Пожалуйста выберите topic для подписки",
|
||||||
"keep_alive": "Keep Alive",
|
"keep_alive": "Поддерживать соединение",
|
||||||
"log": "Лог",
|
"log": "Лог",
|
||||||
"lw_message": "Last-Will Message",
|
"lw_message": "Last-Will Message",
|
||||||
"lw_qos": "Last-Will QoS",
|
"lw_qos": "Last-Will QoS",
|
||||||
"lw_retain": "Last-Will Retain",
|
"lw_retain": "Last-Will Retain",
|
||||||
"lw_topic": "Last-Will Topic",
|
"lw_topic": "Last-Will Topic",
|
||||||
"message": "Сообщение",
|
"message": "Сообщение",
|
||||||
"new": "New Subscription",
|
"new": "Новая подписка",
|
||||||
"not_connected": "Please start a MQTT connection first.",
|
"not_connected": "Пожалуйста, сначала запустите MQTT соединение.",
|
||||||
"publish": "Публиковать",
|
"publish": "Публиковать",
|
||||||
"qos": "QoS",
|
"qos": "QoS",
|
||||||
"ssl": "SSL",
|
"ssl": "SSL",
|
||||||
@@ -355,7 +410,7 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"doc": "Документы",
|
"doc": "Документы",
|
||||||
"graphql": "GraphQL",
|
"graphql": "GraphQL",
|
||||||
"profile": "Profile",
|
"profile": "Профиль",
|
||||||
"realtime": "В реальном времени",
|
"realtime": "В реальном времени",
|
||||||
"rest": "REST",
|
"rest": "REST",
|
||||||
"settings": "Настройки"
|
"settings": "Настройки"
|
||||||
@@ -363,12 +418,12 @@
|
|||||||
"preRequest": {
|
"preRequest": {
|
||||||
"javascript_code": "Код JavaScript",
|
"javascript_code": "Код JavaScript",
|
||||||
"learn": "Читать документацию",
|
"learn": "Читать документацию",
|
||||||
"script": "Сценарий предварительного запроса",
|
"script": "Предворительный скрипт запроса",
|
||||||
"snippets": "Фрагменты"
|
"snippets": "Готовый код"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"app_settings": "Настройки приложения",
|
"app_settings": "Настройки приложения",
|
||||||
"default_hopp_displayname": "Unnamed User",
|
"default_hopp_displayname": "Безымянный",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
"editor_description": "Редакторы могут добавлять, редактировать, а так же удалять запросы.",
|
"editor_description": "Редакторы могут добавлять, редактировать, а так же удалять запросы.",
|
||||||
"email_verification_mail": "На вашу электронную почту отправлено письмо для подтверждения. Перейдите по ссылке из письма, чтобы подтвердить свой электронный адрес.",
|
"email_verification_mail": "На вашу электронную почту отправлено письмо для подтверждения. Перейдите по ссылке из письма, чтобы подтвердить свой электронный адрес.",
|
||||||
@@ -391,13 +446,13 @@
|
|||||||
"choose_language": "Выберите язык",
|
"choose_language": "Выберите язык",
|
||||||
"content_type": "Тип содержимого",
|
"content_type": "Тип содержимого",
|
||||||
"content_type_titles": {
|
"content_type_titles": {
|
||||||
"others": "Others",
|
"others": "Другие",
|
||||||
"structured": "Structured",
|
"structured": "Структурированный",
|
||||||
"text": "Text"
|
"text": "Текст"
|
||||||
},
|
},
|
||||||
"copy_link": "Копировать ссылку",
|
"copy_link": "Копировать ссылку",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Нельзя изменять порядок запросов из разных коллекций",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Запрос скопирован",
|
||||||
"duration": "Продолжительность",
|
"duration": "Продолжительность",
|
||||||
"enter_curl": "Введите cURL",
|
"enter_curl": "Введите cURL",
|
||||||
"generate_code": "Сгенерировать код",
|
"generate_code": "Сгенерировать код",
|
||||||
@@ -405,13 +460,13 @@
|
|||||||
"header_list": "Список заголовков",
|
"header_list": "Список заголовков",
|
||||||
"invalid_name": "Укажите имя для запроса",
|
"invalid_name": "Укажите имя для запроса",
|
||||||
"method": "Методика",
|
"method": "Методика",
|
||||||
"moved": "Request moved",
|
"moved": "Запрос перемещён",
|
||||||
"name": "Имя запроса",
|
"name": "Имя запроса",
|
||||||
"new": "New Request",
|
"new": "Новый запрос",
|
||||||
"order_changed": "Request Order Updated",
|
"order_changed": "Порядок запроса изменён",
|
||||||
"override": "Override",
|
"override": "Переопределить",
|
||||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
"override_help": "Установить <kbd>Content-Type</kbd> в Заголовках",
|
||||||
"overriden": "Overridden",
|
"overriden": "Переопределено",
|
||||||
"parameter_list": "Параметры запроса",
|
"parameter_list": "Параметры запроса",
|
||||||
"parameters": "Параметры",
|
"parameters": "Параметры",
|
||||||
"path": "Путь",
|
"path": "Путь",
|
||||||
@@ -424,17 +479,17 @@
|
|||||||
"save_as": "Сохранить как",
|
"save_as": "Сохранить как",
|
||||||
"saved": "Запрос сохранен",
|
"saved": "Запрос сохранен",
|
||||||
"share": "Делиться",
|
"share": "Делиться",
|
||||||
"share_description": "Share Hoppscotch with your friends",
|
"share_description": "Поделиться Hoppscotch с друзьями",
|
||||||
"title": "Запрос",
|
"title": "Запрос",
|
||||||
"type": "Тип запроса",
|
"type": "Тип запроса",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"variables": "Переменные",
|
"variables": "Переменные",
|
||||||
"view_my_links": "View my links"
|
"view_my_links": "Посмотреть мои ссылки"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"audio": "Audio",
|
"audio": "Аудио",
|
||||||
"body": "Тело ответа",
|
"body": "Тело ответа",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Отфильтровать ответ в формате JSON (используется синтаксис JSONPath)",
|
||||||
"headers": "Заголовки",
|
"headers": "Заголовки",
|
||||||
"html": "HTML",
|
"html": "HTML",
|
||||||
"image": "Изображение",
|
"image": "Изображение",
|
||||||
@@ -446,14 +501,14 @@
|
|||||||
"status": "Статус",
|
"status": "Статус",
|
||||||
"time": "Время",
|
"time": "Время",
|
||||||
"title": "Ответ",
|
"title": "Ответ",
|
||||||
"video": "Video",
|
"video": "Видео",
|
||||||
"waiting_for_connection": "Ожидание соединения",
|
"waiting_for_connection": "Ожидание соединения",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"accent_color": "Основной цвет",
|
"accent_color": "Основной цвет",
|
||||||
"account": "Счет",
|
"account": "Счет",
|
||||||
"account_deleted": "Your account has been deleted",
|
"account_deleted": "Ваш аккаунт был удалён",
|
||||||
"account_description": "Настройте параметры своей учетной записи.",
|
"account_description": "Настройте параметры своей учетной записи.",
|
||||||
"account_email_description": "Ваш основной адрес электронной почты.",
|
"account_email_description": "Ваш основной адрес электронной почты.",
|
||||||
"account_name_description": "Это ваше отображаемое имя.",
|
"account_name_description": "Это ваше отображаемое имя.",
|
||||||
@@ -462,8 +517,8 @@
|
|||||||
"change_font_size": "Изменить размер шрифта",
|
"change_font_size": "Изменить размер шрифта",
|
||||||
"choose_language": "Выберите язык",
|
"choose_language": "Выберите язык",
|
||||||
"dark_mode": "Темный",
|
"dark_mode": "Темный",
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Удалить аккаунт",
|
||||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
"delete_account_description": "Удаление аккаунта нельзя отменить",
|
||||||
"expand_navigation": "Раскрыть панель навигации",
|
"expand_navigation": "Раскрыть панель навигации",
|
||||||
"experiments": "Эксперименты",
|
"experiments": "Эксперименты",
|
||||||
"experiments_notice": "Это набор экспериментов, над которыми мы работаем, которые могут оказаться полезными, интересными, и тем, и другим, или ни тем, ни другим. Они не окончательные и могут быть нестабильными, поэтому, если произойдет что-то слишком странное, не паникуйте. Просто выключи эту чертову штуку. Шутки в сторону,",
|
"experiments_notice": "Это набор экспериментов, над которыми мы работаем, которые могут оказаться полезными, интересными, и тем, и другим, или ни тем, ни другим. Они не окончательные и могут быть нестабильными, поэтому, если произойдет что-то слишком странное, не паникуйте. Просто выключи эту чертову штуку. Шутки в сторону,",
|
||||||
@@ -490,8 +545,8 @@
|
|||||||
"proxy_use_toggle": "Используйте промежуточное ПО прокси для отправки запросов",
|
"proxy_use_toggle": "Используйте промежуточное ПО прокси для отправки запросов",
|
||||||
"read_the": "Прочтите",
|
"read_the": "Прочтите",
|
||||||
"reset_default": "Восстановление значений по умолчанию",
|
"reset_default": "Восстановление значений по умолчанию",
|
||||||
"short_codes": "Short codes",
|
"short_codes": "Короткие ссылки",
|
||||||
"short_codes_description": "Short codes which were created by you.",
|
"short_codes_description": "Короткие ссылки, созданные вами",
|
||||||
"sidebar_on_left": "Панель слева",
|
"sidebar_on_left": "Панель слева",
|
||||||
"sync": "Синхронизировать",
|
"sync": "Синхронизировать",
|
||||||
"sync_collections": "Коллекции",
|
"sync_collections": "Коллекции",
|
||||||
@@ -505,16 +560,16 @@
|
|||||||
"theme_description": "Настройте тему своего приложения.",
|
"theme_description": "Настройте тему своего приложения.",
|
||||||
"use_experimental_url_bar": "Использовать экспериментальную строку URL с выделением среды",
|
"use_experimental_url_bar": "Использовать экспериментальную строку URL с выделением среды",
|
||||||
"user": "Пользователь",
|
"user": "Пользователь",
|
||||||
"verified_email": "Verified email",
|
"verified_email": "Проверенный Email",
|
||||||
"verify_email": "Подтвердить почту"
|
"verify_email": "Подтвердить Email"
|
||||||
},
|
},
|
||||||
"shortcodes": {
|
"shortcodes": {
|
||||||
"actions": "Actions",
|
"actions": "Действия",
|
||||||
"created_on": "Created on",
|
"created_on": "Создано",
|
||||||
"deleted": "Shortcode deleted",
|
"deleted": "Удалёна",
|
||||||
"method": "Method",
|
"method": "Метод",
|
||||||
"not_found": "Shortcode not found",
|
"not_found": "Короткая ссылка не найдена",
|
||||||
"short_code": "Short code",
|
"short_code": "Короткая ссылка",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"shortcut": {
|
"shortcut": {
|
||||||
@@ -525,6 +580,10 @@
|
|||||||
"show_all": "Горячие клавиши",
|
"show_all": "Горячие клавиши",
|
||||||
"title": "Общий"
|
"title": "Общий"
|
||||||
},
|
},
|
||||||
|
"others": {
|
||||||
|
"title": "Другие",
|
||||||
|
"prettify": "Прекрасные редакторы"
|
||||||
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Пригласите людей в Hoppscotch",
|
"invite": "Пригласите людей в Hoppscotch",
|
||||||
"title": "Разное"
|
"title": "Разное"
|
||||||
@@ -545,6 +604,9 @@
|
|||||||
"delete_method": "Выберите метод DELETE",
|
"delete_method": "Выберите метод DELETE",
|
||||||
"get_method": "Выберите метод GET",
|
"get_method": "Выберите метод GET",
|
||||||
"head_method": "Выберите метод HEAD",
|
"head_method": "Выберите метод HEAD",
|
||||||
|
"rename": "Переименовать запрос",
|
||||||
|
"import_curl": "Импорт из cURL",
|
||||||
|
"show_code": "Сгенерировать готовый код",
|
||||||
"method": "Методика",
|
"method": "Методика",
|
||||||
"next_method": "Выберите следующий метод",
|
"next_method": "Выберите следующий метод",
|
||||||
"post_method": "Выберите метод POST",
|
"post_method": "Выберите метод POST",
|
||||||
@@ -553,35 +615,120 @@
|
|||||||
"reset_request": "Сбросить запрос",
|
"reset_request": "Сбросить запрос",
|
||||||
"save_to_collections": "Сохранить в коллекции",
|
"save_to_collections": "Сохранить в коллекции",
|
||||||
"send_request": "Послать запрос",
|
"send_request": "Послать запрос",
|
||||||
|
"save_request": "Сохарнить запрос",
|
||||||
"title": "Запрос"
|
"title": "Запрос"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response to clipboard",
|
"copy": "Копировать запрос в буфер обмена",
|
||||||
"download": "Download response as file",
|
"download": "Скачать запрос как файл",
|
||||||
"title": "Response"
|
"title": "Запрос"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Switch theme to black mode",
|
"black": "Черный режим",
|
||||||
"dark": "Switch theme to dark mode",
|
"dark": "Тёмный режим",
|
||||||
"light": "Switch theme to light mode",
|
"light": "Светлый режим",
|
||||||
"system": "Switch theme to system mode",
|
"system": "Определяется системой",
|
||||||
"title": "Theme"
|
"title": "Тема"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"code": "Показать код",
|
"code": "Показать код",
|
||||||
"collection": "Expand Collection Panel",
|
"collection": "Развернуть панель коллекций",
|
||||||
"more": "Показать больше",
|
"more": "Показать больше",
|
||||||
"sidebar": "Показать боковую панель"
|
"sidebar": "Показать боковую панель"
|
||||||
},
|
},
|
||||||
"socketio": {
|
"socketio": {
|
||||||
"communication": "Коммуникация",
|
"communication": "Коммуникация",
|
||||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
"connection_not_authorized": "Это SocketIO соединение не использует какую-либо авторизацию.",
|
||||||
"event_name": "Название события",
|
"event_name": "Название события",
|
||||||
"events": "События",
|
"events": "События",
|
||||||
"log": "Лог",
|
"log": "Лог",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
|
"spotlight": {
|
||||||
|
"general": {
|
||||||
|
"help_menu": "Помощь и поддержка",
|
||||||
|
"chat": "Чат с поддержкой",
|
||||||
|
"open_docs": "Прочитать документацию",
|
||||||
|
"open_keybindings": "Горячие клавиши",
|
||||||
|
"open_github": "Открыть GitHub репозиторий",
|
||||||
|
"social": "Соц. сети",
|
||||||
|
"title": "Основное"
|
||||||
|
},
|
||||||
|
"miscellaneous": {
|
||||||
|
"invite": "Пригласите друзей в Hoppscotch",
|
||||||
|
"title": "Разное"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"switch_to": "Перейти к",
|
||||||
|
"select_method": "Выбрать метод",
|
||||||
|
"save_as_new": "Сохранить как новый запрос",
|
||||||
|
"tab_parameters": "Параметры",
|
||||||
|
"tab_body": "Тело",
|
||||||
|
"tab_headers": "Заголовки",
|
||||||
|
"tab_authorization": "Авторизация",
|
||||||
|
"tab_pre_request_script": "Пред-скрипт",
|
||||||
|
"tab_tests": "Тесты",
|
||||||
|
"tab_query": "Запрос",
|
||||||
|
"tab_variables": "Переменные"
|
||||||
|
},
|
||||||
|
"graphql": {
|
||||||
|
"connect": "Соединиться",
|
||||||
|
"disconnect": "Разъединить"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"copy": "Копировать ответ",
|
||||||
|
"download": "Скачать ответ",
|
||||||
|
"title": "Ответ"
|
||||||
|
},
|
||||||
|
"environments": {
|
||||||
|
"new": "Создать новое окружение",
|
||||||
|
"new_variable": "Создать новую переменную в окружении",
|
||||||
|
"edit": "Изменить текущее окружение",
|
||||||
|
"delete": "Удалить текущее окружение",
|
||||||
|
"duplicate": "Скопировать текущее окружение",
|
||||||
|
"edit_global": "Изменить глобальное окружение",
|
||||||
|
"duplicate_global": "Скопировать глобальное окружение",
|
||||||
|
"title": "Окружения"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"new": "Создать новую команду",
|
||||||
|
"edit": "Изменить текущую команду",
|
||||||
|
"delete": "Удалить текущую команду",
|
||||||
|
"invite": "Пригласить людей в команду",
|
||||||
|
"switch_to_personal": "Переключиться на личное пространство",
|
||||||
|
"title": "Команды"
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"duplicate": "Скопировать вкладку",
|
||||||
|
"close_current": "Закрыть вкладку",
|
||||||
|
"close_others": "Закрыть все кроме этой",
|
||||||
|
"new_tab": "Открыть в новой вкладке",
|
||||||
|
"title": "Вкладки"
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
"user": "Пользователь",
|
||||||
|
"theme": "Тема",
|
||||||
|
"interface": "Интерфейс",
|
||||||
|
"interceptor": "Перехватчик"
|
||||||
|
},
|
||||||
|
"change_language": "Изменить язык",
|
||||||
|
"settings": {
|
||||||
|
"theme": {
|
||||||
|
"black": "Чёрная",
|
||||||
|
"dark": "Тёмная",
|
||||||
|
"light": "Светлая",
|
||||||
|
"system": "Определяется системой"
|
||||||
|
},
|
||||||
|
"font": {
|
||||||
|
"size_sm": "Маленький",
|
||||||
|
"size_md": "Средний",
|
||||||
|
"size_lg": "Большой"
|
||||||
|
},
|
||||||
|
"change_interceptor": "Изменить перехватчик",
|
||||||
|
"change_language": "Изменить язык"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
"event_type": "Тип события",
|
"event_type": "Тип события",
|
||||||
"log": "Лог",
|
"log": "Лог",
|
||||||
@@ -589,14 +736,14 @@
|
|||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"bulk_mode": "Множественное редактирование",
|
"bulk_mode": "Множественное редактирование",
|
||||||
"bulk_mode_placeholder": "Entries are separated by newline\nKeys and values are separated by :\nPrepend # to any row you want to add but keep disabled",
|
"bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разедляются двоеточием\nИспользуйте # для комментария",
|
||||||
"cleared": "Очищено",
|
"cleared": "Очищено",
|
||||||
"connected": "Связаны",
|
"connected": "Связаны",
|
||||||
"connected_to": "Подключено к {name}",
|
"connected_to": "Подключено к {name}",
|
||||||
"connecting_to": "Подключение к {name} ...",
|
"connecting_to": "Подключение к {name} ...",
|
||||||
"connection_error": "Failed to connect",
|
"connection_error": "Ошибка подключения",
|
||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Не удалось установить соединение",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "Соединение утеряно",
|
||||||
"copied_to_clipboard": "Скопировано в буфер обмена",
|
"copied_to_clipboard": "Скопировано в буфер обмена",
|
||||||
"deleted": "Удалено",
|
"deleted": "Удалено",
|
||||||
"deprecated": "УСТАРЕЛО",
|
"deprecated": "УСТАРЕЛО",
|
||||||
@@ -611,17 +758,17 @@
|
|||||||
"history_deleted": "История удалена",
|
"history_deleted": "История удалена",
|
||||||
"linewrap": "Обернуть линии",
|
"linewrap": "Обернуть линии",
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
"message_received": "Сообщение: {message} получено по топику: {topic}",
|
||||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
"mqtt_subscription_failed": "Что-то пошло не так, при попытке подписаться на топик: {topic}",
|
||||||
"none": "Никто",
|
"none": "Никто",
|
||||||
"nothing_found": "Ничего не найдено для",
|
"nothing_found": "Ничего не найдено для",
|
||||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
"published_error": "Что-то пошло не так при попытке опубликовать сообщение в топик {topic}: {message}",
|
||||||
"published_message": "Published message: {message} to topic: {topic}",
|
"published_message": "Опубликовано сообщение: {message} в топик: {topic}",
|
||||||
"reconnection_error": "Failed to reconnect",
|
"reconnection_error": "Не удалось переподключиться",
|
||||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
"subscribed_failed": "Не удалось подписаться на топик: {topic}",
|
||||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
"subscribed_success": "Успешно подписался на топик: {topic}",
|
||||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
"unsubscribed_failed": "Не удалось отписаться от топика: {topic}",
|
||||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
"unsubscribed_success": "Успешно отписался от топика: {topic}",
|
||||||
"waiting_send_request": "Ожидание отправки запроса"
|
"waiting_send_request": "Ожидание отправки запроса"
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
@@ -630,7 +777,7 @@
|
|||||||
"community": "Задавайте вопросы и помогайте другим",
|
"community": "Задавайте вопросы и помогайте другим",
|
||||||
"documentation": "Узнать больше о Hoppscotch",
|
"documentation": "Узнать больше о Hoppscotch",
|
||||||
"forum": "Задавайте вопросы и получайте ответы",
|
"forum": "Задавайте вопросы и получайте ответы",
|
||||||
"github": "Follow us on Github",
|
"github": "Подпишитесь на нас на Github",
|
||||||
"shortcuts": "Просматривайте приложение быстрее",
|
"shortcuts": "Просматривайте приложение быстрее",
|
||||||
"team": "Свяжитесь с командой",
|
"team": "Свяжитесь с командой",
|
||||||
"title": "Служба поддержки",
|
"title": "Служба поддержки",
|
||||||
@@ -641,15 +788,15 @@
|
|||||||
"body": "Тело",
|
"body": "Тело",
|
||||||
"collections": "Коллекции",
|
"collections": "Коллекции",
|
||||||
"documentation": "Документация",
|
"documentation": "Документация",
|
||||||
"environments": "Environments",
|
"environments": "Окружения",
|
||||||
"headers": "Заголовки",
|
"headers": "Заголовки",
|
||||||
"history": "История",
|
"history": "История",
|
||||||
"mqtt": "MQTT",
|
"mqtt": "MQTT",
|
||||||
"parameters": "Параметры",
|
"parameters": "Параметры",
|
||||||
"pre_request_script": "Скрипт предварительного запроса",
|
"pre_request_script": "Пред-скрипт",
|
||||||
"queries": "Запросы",
|
"queries": "Запросы",
|
||||||
"query": "Запрос",
|
"query": "Запрос",
|
||||||
"schema": "Schema",
|
"schema": "Схема",
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Тесты",
|
"tests": "Тесты",
|
||||||
@@ -666,7 +813,7 @@
|
|||||||
"email_do_not_match": "Электронная почта, которой Вы воспользовались не соответсвует указанной в данных Вашей учетной записи.",
|
"email_do_not_match": "Электронная почта, которой Вы воспользовались не соответсвует указанной в данных Вашей учетной записи.",
|
||||||
"exit": "Выйти из команды",
|
"exit": "Выйти из команды",
|
||||||
"exit_disabled": "Только владелец не может выйти из команды",
|
"exit_disabled": "Только владелец не может выйти из команды",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "Не верный идентификатор коллекции",
|
||||||
"invalid_email_format": "Формат электронной почты недействителен",
|
"invalid_email_format": "Формат электронной почты недействителен",
|
||||||
"invalid_id": "Некорректный ID команды. Свяжитесь с руководителем команды.",
|
"invalid_id": "Некорректный ID команды. Свяжитесь с руководителем команды.",
|
||||||
"invalid_invite_link": "Ссылка недействительна",
|
"invalid_invite_link": "Ссылка недействительна",
|
||||||
@@ -690,7 +837,7 @@
|
|||||||
"member_removed": "Пользователь удален",
|
"member_removed": "Пользователь удален",
|
||||||
"member_role_updated": "Роли пользователей обновлены",
|
"member_role_updated": "Роли пользователей обновлены",
|
||||||
"members": "Участники",
|
"members": "Участники",
|
||||||
"more_members": "+{count} more",
|
"more_members": "+{count}",
|
||||||
"name_length_insufficient": "Название команды должно быть не менее 6 символов.",
|
"name_length_insufficient": "Название команды должно быть не менее 6 символов.",
|
||||||
"name_updated": "Название команды обновлено",
|
"name_updated": "Название команды обновлено",
|
||||||
"new": "Новая команда",
|
"new": "Новая команда",
|
||||||
@@ -698,13 +845,13 @@
|
|||||||
"new_name": "Моя новая команда",
|
"new_name": "Моя новая команда",
|
||||||
"no_access": "У вас нет прав на редактирование этих коллекций",
|
"no_access": "У вас нет прав на редактирование этих коллекций",
|
||||||
"no_invite_found": "Такое приглашение мы не смогли найти. Свяжитесь с руководителем команды.",
|
"no_invite_found": "Такое приглашение мы не смогли найти. Свяжитесь с руководителем команды.",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "Запрос не найден",
|
||||||
"not_found": "Team not found. Contact your team owner.",
|
"not_found": "Команда не найдена, свяжитесь с владельцем команды",
|
||||||
"not_valid_viewer": "У Вас нет прав просматривать это. Свяжитесь с руководителем команды.",
|
"not_valid_viewer": "У Вас нет прав просматривать это. Свяжитесь с руководителем команды.",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "Не удалось переместить коллекцию в дочернюю",
|
||||||
"pending_invites": "Ожидающие приглашения",
|
"pending_invites": "Ожидающие приглашения",
|
||||||
"permissions": "Разрешения",
|
"permissions": "Разрешения",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "Таже цель и конечная точка",
|
||||||
"saved": "Команда сохранена",
|
"saved": "Команда сохранена",
|
||||||
"select_a_team": "Выбрать команду",
|
"select_a_team": "Выбрать команду",
|
||||||
"title": "Команды",
|
"title": "Команды",
|
||||||
@@ -712,9 +859,9 @@
|
|||||||
"we_sent_invite_link_description": "Попросите тех, кого Вы пригласили, проверить их почтовые ящики. Им нужно перейди по ссылке, чтобы подтвердить вступление в эту команду."
|
"we_sent_invite_link_description": "Попросите тех, кого Вы пригласили, проверить их почтовые ящики. Им нужно перейди по ссылке, чтобы подтвердить вступление в эту команду."
|
||||||
},
|
},
|
||||||
"team_environment": {
|
"team_environment": {
|
||||||
"deleted": "Environment Deleted",
|
"deleted": "Окружение удалено",
|
||||||
"duplicate": "Environment Duplicated",
|
"duplicate": "Окружение скопировано",
|
||||||
"not_found": "Environment not found."
|
"not_found": "Окружение не найдено"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"failed": "Тест не пройден",
|
"failed": "Тест не пройден",
|
||||||
@@ -734,9 +881,9 @@
|
|||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "Изменить пространство",
|
||||||
"personal": "My Workspace",
|
"personal": "Моё пространство",
|
||||||
"team": "Team Workspace",
|
"team": "Пространство команды",
|
||||||
"title": "Workspaces"
|
"title": "Рабочие пространства"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"filter": "篩選回應",
|
"filter": "篩選回應",
|
||||||
"go_back": "返回",
|
"go_back": "返回",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "向前",
|
||||||
"group_by": "分組方式",
|
"group_by": "分組方式",
|
||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"learn_more": "瞭解更多",
|
"learn_more": "瞭解更多",
|
||||||
@@ -117,37 +117,37 @@
|
|||||||
"username": "使用者名稱"
|
"username": "使用者名稱"
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "組合已建立",
|
"created": "集合已建立",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "無法為父集合不同的集合重新排序",
|
||||||
"edit": "編輯組合",
|
"edit": "編輯集合",
|
||||||
"invalid_name": "請提供有效的組合名稱",
|
"invalid_name": "請提供有效的集合名稱",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "集合已在根目錄",
|
||||||
"moved": "Moved Successfully",
|
"moved": "移動成功",
|
||||||
"my_collections": "我的組合",
|
"my_collections": "我的集合",
|
||||||
"name": "我的新組合",
|
"name": "我的新集合",
|
||||||
"name_length_insufficient": "組合名稱至少要有 3 個字元。",
|
"name_length_insufficient": "集合名稱至少要有 3 個字元。",
|
||||||
"new": "建立組合",
|
"new": "建立集合",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "集合順序已更新",
|
||||||
"renamed": "組合已重新命名",
|
"renamed": "集合已重新命名",
|
||||||
"request_in_use": "請求正在使用中",
|
"request_in_use": "請求正在使用中",
|
||||||
"save_as": "另存為",
|
"save_as": "另存為",
|
||||||
"select": "選擇一個組合",
|
"select": "選擇一個集合",
|
||||||
"select_location": "選擇位置",
|
"select_location": "選擇位置",
|
||||||
"select_team": "選擇一個團隊",
|
"select_team": "選擇一個團隊",
|
||||||
"team_collections": "團隊組合"
|
"team_collections": "團隊集合"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"exit_team": "您確定要離開此團隊嗎?",
|
"exit_team": "您確定要離開此團隊嗎?",
|
||||||
"logout": "您確定要登出嗎?",
|
"logout": "您確定要登出嗎?",
|
||||||
"remove_collection": "您確定要永久刪除該組合嗎?",
|
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||||
"remove_environment": "您確定要永久刪除該環境嗎?",
|
"remove_environment": "您確定要永久刪除該環境嗎?",
|
||||||
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
||||||
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
||||||
"remove_request": "您確定要永久刪除該請求嗎?",
|
"remove_request": "您確定要永久刪除該請求嗎?",
|
||||||
"remove_team": "您確定要刪除該團隊嗎?",
|
"remove_team": "您確定要刪除該團隊嗎?",
|
||||||
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
||||||
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
|
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
|
||||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"generate": "產生文件",
|
"generate": "產生文件",
|
||||||
"generate_message": "匯入 Hoppscotch 組合以隨時隨地產生 API 文件。"
|
"generate_message": "匯入 Hoppscotch 集合以隨時隨地產生 API 文件。"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"authorization": "該請求沒有使用任何授權",
|
"authorization": "該請求沒有使用任何授權",
|
||||||
"body": "該請求沒有任何請求主體",
|
"body": "該請求沒有任何請求主體",
|
||||||
"collection": "組合為空",
|
"collection": "集合為空",
|
||||||
"collections": "組合為空",
|
"collections": "集合為空",
|
||||||
"documentation": "連線到 GraphQL 端點以檢視文件",
|
"documentation": "連線到 GraphQL 端點以檢視文件",
|
||||||
"endpoint": "端點不能留空",
|
"endpoint": "端點不能留空",
|
||||||
"environments": "環境為空",
|
"environments": "環境為空",
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
||||||
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
||||||
"curl_invalid_format": "cURL 格式不正確",
|
"curl_invalid_format": "cURL 格式不正確",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "危險地帶",
|
||||||
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
||||||
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
||||||
"empty_req_name": "空請求名稱",
|
"empty_req_name": "空請求名稱",
|
||||||
@@ -277,38 +277,38 @@
|
|||||||
"tests": "編寫測試指令碼以自動除錯。"
|
"tests": "編寫測試指令碼以自動除錯。"
|
||||||
},
|
},
|
||||||
"hide": {
|
"hide": {
|
||||||
"collection": "隱藏組合面板",
|
"collection": "隱藏集合面板",
|
||||||
"more": "隱藏更多",
|
"more": "隱藏更多",
|
||||||
"preview": "隱藏預覽",
|
"preview": "隱藏預覽",
|
||||||
"sidebar": "隱藏側邊欄"
|
"sidebar": "隱藏側邊欄"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "匯入組合",
|
"collections": "匯入集合",
|
||||||
"curl": "匯入 cURL",
|
"curl": "匯入 cURL",
|
||||||
"failed": "匯入失敗",
|
"failed": "匯入失敗",
|
||||||
"from_gist": "從 Gist 匯入",
|
"from_gist": "從 Gist 匯入",
|
||||||
"from_gist_description": "從 Gist 網址匯入",
|
"from_gist_description": "從 Gist 網址匯入",
|
||||||
"from_insomnia": "從 Insomnia 匯入",
|
"from_insomnia": "從 Insomnia 匯入",
|
||||||
"from_insomnia_description": "從 Insomnia 組合匯入",
|
"from_insomnia_description": "從 Insomnia 集合匯入",
|
||||||
"from_json": "從 Hoppscotch 匯入",
|
"from_json": "從 Hoppscotch 匯入",
|
||||||
"from_json_description": "從 Hoppscotch 組合檔匯入",
|
"from_json_description": "從 Hoppscotch 集合檔匯入",
|
||||||
"from_my_collections": "從我的組合匯入",
|
"from_my_collections": "從我的集合匯入",
|
||||||
"from_my_collections_description": "從我的組合檔匯入",
|
"from_my_collections_description": "從我的集合檔匯入",
|
||||||
"from_openapi": "從 OpenAPI 匯入",
|
"from_openapi": "從 OpenAPI 匯入",
|
||||||
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
||||||
"from_postman": "從 Postman 匯入",
|
"from_postman": "從 Postman 匯入",
|
||||||
"from_postman_description": "從 Postman 組合匯入",
|
"from_postman_description": "從 Postman 集合匯入",
|
||||||
"from_url": "從網址匯入",
|
"from_url": "從網址匯入",
|
||||||
"gist_url": "輸入 Gist 網址",
|
"gist_url": "輸入 Gist 網址",
|
||||||
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
"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_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||||
"import_from_url_success": "已匯入組合",
|
"import_from_url_success": "已匯入集合",
|
||||||
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
|
"json_description": "從 Hoppscotch 集合 JSON 檔匯入集合",
|
||||||
"title": "匯入"
|
"title": "匯入"
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"collapse_collection": "隱藏或顯示組合",
|
"collapse_collection": "隱藏或顯示集合",
|
||||||
"collapse_sidebar": "隱藏或顯示側邊欄",
|
"collapse_sidebar": "隱藏或顯示側邊欄",
|
||||||
"column": "垂直版面",
|
"column": "垂直版面",
|
||||||
"name": "配置",
|
"name": "配置",
|
||||||
@@ -316,8 +316,8 @@
|
|||||||
"zen_mode": "專注模式"
|
"zen_mode": "專注模式"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "您有未儲存的改動",
|
||||||
"collections": "組合",
|
"collections": "集合",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"edit_request": "編輯請求",
|
"edit_request": "編輯請求",
|
||||||
"import_export": "匯入/匯出"
|
"import_export": "匯入/匯出"
|
||||||
@@ -374,9 +374,9 @@
|
|||||||
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
||||||
"no_permission": "您沒有權限執行此操作。",
|
"no_permission": "您沒有權限執行此操作。",
|
||||||
"owner": "擁有者",
|
"owner": "擁有者",
|
||||||
"owner_description": "擁有者可以新增、編輯和刪除請求、組合和團隊成員。",
|
"owner_description": "擁有者可以新增、編輯和刪除請求、集合和團隊成員。",
|
||||||
"roles": "角色",
|
"roles": "角色",
|
||||||
"roles_description": "角色用來控制對共用組合的存取權。",
|
"roles_description": "角色用來控制對共用集合的存取權。",
|
||||||
"updated": "已更新個人檔案",
|
"updated": "已更新個人檔案",
|
||||||
"viewer": "檢視者",
|
"viewer": "檢視者",
|
||||||
"viewer_description": "檢視者只能檢視和使用請求。"
|
"viewer_description": "檢視者只能檢視和使用請求。"
|
||||||
@@ -396,8 +396,8 @@
|
|||||||
"text": "文字"
|
"text": "文字"
|
||||||
},
|
},
|
||||||
"copy_link": "複製連結",
|
"copy_link": "複製連結",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "無法重新排列來自不同集合的請求",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "已複製請求",
|
||||||
"duration": "持續時間",
|
"duration": "持續時間",
|
||||||
"enter_curl": "輸入 cURL",
|
"enter_curl": "輸入 cURL",
|
||||||
"generate_code": "產生程式碼",
|
"generate_code": "產生程式碼",
|
||||||
@@ -405,10 +405,10 @@
|
|||||||
"header_list": "請求標頭列表",
|
"header_list": "請求標頭列表",
|
||||||
"invalid_name": "請提供請求名稱",
|
"invalid_name": "請提供請求名稱",
|
||||||
"method": "方法",
|
"method": "方法",
|
||||||
"moved": "Request moved",
|
"moved": "已移動請求",
|
||||||
"name": "請求名稱",
|
"name": "請求名稱",
|
||||||
"new": "新請求",
|
"new": "新請求",
|
||||||
"order_changed": "Request Order Updated",
|
"order_changed": "已更新請求順序",
|
||||||
"override": "覆寫",
|
"override": "覆寫",
|
||||||
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
||||||
"overriden": "已覆寫",
|
"overriden": "已覆寫",
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
"view_my_links": "檢視我的連結"
|
"view_my_links": "檢視我的連結"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"audio": "Audio",
|
"audio": "音訊",
|
||||||
"body": "回應本體",
|
"body": "回應本體",
|
||||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||||
"headers": "回應標頭",
|
"headers": "回應標頭",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"title": "回應",
|
"title": "回應",
|
||||||
"video": "Video",
|
"video": "視訊",
|
||||||
"waiting_for_connection": "等待連線",
|
"waiting_for_connection": "等待連線",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
"short_codes_description": "我們為您打造的快捷碼。",
|
"short_codes_description": "我們為您打造的快捷碼。",
|
||||||
"sidebar_on_left": "左側邊欄",
|
"sidebar_on_left": "左側邊欄",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"sync_collections": "組合",
|
"sync_collections": "集合",
|
||||||
"sync_description": "這些設定會同步到雲端。",
|
"sync_description": "這些設定會同步到雲端。",
|
||||||
"sync_environments": "環境",
|
"sync_environments": "環境",
|
||||||
"sync_history": "歷史",
|
"sync_history": "歷史",
|
||||||
@@ -551,7 +551,7 @@
|
|||||||
"previous_method": "選擇上一個方法",
|
"previous_method": "選擇上一個方法",
|
||||||
"put_method": "選擇 PUT 方法",
|
"put_method": "選擇 PUT 方法",
|
||||||
"reset_request": "重置請求",
|
"reset_request": "重置請求",
|
||||||
"save_to_collections": "儲存到組合",
|
"save_to_collections": "儲存到集合",
|
||||||
"send_request": "傳送請求",
|
"send_request": "傳送請求",
|
||||||
"title": "請求"
|
"title": "請求"
|
||||||
},
|
},
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"code": "顯示程式碼",
|
"code": "顯示程式碼",
|
||||||
"collection": "顯示組合面板",
|
"collection": "顯示集合面板",
|
||||||
"more": "顯示更多",
|
"more": "顯示更多",
|
||||||
"sidebar": "顯示側邊欄"
|
"sidebar": "顯示側邊欄"
|
||||||
},
|
},
|
||||||
@@ -639,9 +639,9 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "授權",
|
"authorization": "授權",
|
||||||
"body": "請求本體",
|
"body": "請求本體",
|
||||||
"collections": "組合",
|
"collections": "集合",
|
||||||
"documentation": "幫助文件",
|
"documentation": "幫助文件",
|
||||||
"environments": "Environments",
|
"environments": "環境",
|
||||||
"headers": "請求標頭",
|
"headers": "請求標頭",
|
||||||
"history": "歷史記錄",
|
"history": "歷史記錄",
|
||||||
"mqtt": "MQTT",
|
"mqtt": "MQTT",
|
||||||
@@ -666,7 +666,7 @@
|
|||||||
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
||||||
"exit": "退出團隊",
|
"exit": "退出團隊",
|
||||||
"exit_disabled": "團隊擁有者無法退出團隊",
|
"exit_disabled": "團隊擁有者無法退出團隊",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "集合 ID 無效",
|
||||||
"invalid_email_format": "電子信箱格式無效",
|
"invalid_email_format": "電子信箱格式無效",
|
||||||
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
||||||
"invalid_invite_link": "邀請連結無效",
|
"invalid_invite_link": "邀請連結無效",
|
||||||
@@ -690,21 +690,21 @@
|
|||||||
"member_removed": "使用者已移除",
|
"member_removed": "使用者已移除",
|
||||||
"member_role_updated": "使用者角色已更新",
|
"member_role_updated": "使用者角色已更新",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"more_members": "+{count} more",
|
"more_members": "還有 {count} 位",
|
||||||
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
||||||
"name_updated": "團隊名稱已更新",
|
"name_updated": "團隊名稱已更新",
|
||||||
"new": "新團隊",
|
"new": "新團隊",
|
||||||
"new_created": "已建立新團隊",
|
"new_created": "已建立新團隊",
|
||||||
"new_name": "我的新團隊",
|
"new_name": "我的新團隊",
|
||||||
"no_access": "您沒有編輯組合的許可權",
|
"no_access": "您沒有編輯集合的許可權",
|
||||||
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "找不到請求。",
|
||||||
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
||||||
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "無法將集合移動至子集合",
|
||||||
"pending_invites": "待定邀請",
|
"pending_invites": "待定邀請",
|
||||||
"permissions": "許可權",
|
"permissions": "許可權",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "目標和目的地相同",
|
||||||
"saved": "團隊已儲存",
|
"saved": "團隊已儲存",
|
||||||
"select_a_team": "選擇團隊",
|
"select_a_team": "選擇團隊",
|
||||||
"title": "團隊",
|
"title": "團隊",
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
"url": "網址"
|
"url": "網址"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "切換工作區",
|
||||||
"personal": "My Workspace",
|
"personal": "我的工作區",
|
||||||
"team": "Team Workspace",
|
"team": "團隊工作區",
|
||||||
"title": "Workspaces"
|
"title": "工作區"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.7",
|
"version": "2023.8.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
||||||
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||||
@@ -13,140 +15,148 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
||||||
"postinstall": "pnpm run gql-codegen",
|
"postinstall": "pnpm run gql-codegen",
|
||||||
|
"do-test": "pnpm run test",
|
||||||
"do-lint": "pnpm run prod-lint",
|
"do-lint": "pnpm run prod-lint",
|
||||||
"do-typecheck": "pnpm run lint",
|
"do-typecheck": "pnpm run lint",
|
||||||
"do-lintfix": "pnpm run lintfix"
|
"do-lintfix": "pnpm run lintfix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.0.3",
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
"@codemirror/commands": "^6.0.1",
|
"@codemirror/commands": "^6.2.4",
|
||||||
"@codemirror/lang-javascript": "^6.0.1",
|
"@codemirror/lang-javascript": "^6.1.9",
|
||||||
"@codemirror/lang-json": "^6.0.0",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-xml": "^6.0.0",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "^6.2.0",
|
"@codemirror/language": "^6.9.0",
|
||||||
"@codemirror/legacy-modes": "^6.1.0",
|
"@codemirror/legacy-modes": "^6.3.3",
|
||||||
"@codemirror/lint": "^6.0.0",
|
"@codemirror/lint": "^6.4.0",
|
||||||
"@codemirror/search": "^6.0.0",
|
"@codemirror/search": "^6.5.1",
|
||||||
"@codemirror/state": "^6.1.0",
|
"@codemirror/state": "^6.2.1",
|
||||||
"@codemirror/view": "^6.0.2",
|
"@codemirror/view": "^6.16.0",
|
||||||
|
"@fontsource-variable/inter": "^5.0.8",
|
||||||
|
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
||||||
|
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "workspace:^",
|
"@hoppscotch/ui": "workspace:^",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
"@lezer/highlight": "^1.0.0",
|
"@lezer/highlight": "^1.1.6",
|
||||||
"@sentry/tracing": "^7.13.0",
|
"@sentry/tracing": "^7.64.0",
|
||||||
"@sentry/vue": "^7.13.0",
|
"@sentry/vue": "^7.64.0",
|
||||||
"@urql/core": "^2.5.0",
|
"@urql/core": "^4.1.1",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^0.1.7",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
"@urql/exchange-graphcache": "^4.4.3",
|
"@urql/exchange-graphcache": "^6.3.2",
|
||||||
"@vitejs/plugin-legacy": "^2.3.0",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vueuse/core": "^8.7.5",
|
"@vueuse/core": "^10.3.0",
|
||||||
"@vueuse/head": "^0.7.9",
|
"@vueuse/head": "^1.3.1",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"axios": "^0.21.4",
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"dioc": "workspace:^",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.12.1",
|
"fp-ts": "^2.16.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "^1.0.3",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^16.8.0",
|
||||||
"graphql-language-service-interface": "^2.9.1",
|
"graphql-language-service-interface": "^2.9.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^2.0.0",
|
"httpsnippet": "^3.0.1",
|
||||||
"insomnia-importers": "^3.3.0",
|
"insomnia-importers": "^3.6.0",
|
||||||
"io-ts": "^2.2.16",
|
"io-ts": "^2.2.20",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonpath-plus": "^7.0.0",
|
"jsonpath-plus": "^7.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^2.0.8",
|
"lossless-json": "^2.0.11",
|
||||||
|
"minisearch": "^6.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postman-collection": "^4.1.4",
|
"postman-collection": "^4.2.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.11.2",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.8.1",
|
||||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
||||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
||||||
"socketio-wildcard": "^2.0.0",
|
"socketio-wildcard": "^2.0.0",
|
||||||
"splitpanes": "^3.1.1",
|
"splitpanes": "^3.1.5",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.1",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.5",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^9.0.0",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.3.4",
|
||||||
"vue-github-button": "^3.0.3",
|
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-pdf-embed": "^1.1.4",
|
"vue-pdf-embed": "^1.1.6",
|
||||||
"vue-router": "^4.0.16",
|
"vue-router": "^4.2.4",
|
||||||
"vue-tippy": "6.0.0-alpha.58",
|
"vue-tippy": "6.3.1",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "^4.1.1",
|
||||||
"wonka": "^4.0.15",
|
"wonka": "^6.3.4",
|
||||||
"workbox-window": "^6.5.4",
|
"workbox-window": "^7.0.0",
|
||||||
"xml-formatter": "^3.4.1",
|
"xml-formatter": "^3.5.0",
|
||||||
"yargs-parser": "^21.1.1"
|
"yargs-parser": "^21.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
"@graphql-codegen/add": "^3.2.0",
|
"@graphql-codegen/add": "^5.0.0",
|
||||||
"@graphql-codegen/cli": "^2.8.0",
|
"@graphql-codegen/cli": "^5.0.0",
|
||||||
"@graphql-codegen/typed-document-node": "^2.3.1",
|
"@graphql-codegen/typed-document-node": "^5.0.1",
|
||||||
"@graphql-codegen/typescript": "^2.7.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
|
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.0",
|
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
"@iconify-json/lucide": "^1.1.40",
|
"@iconify-json/lucide": "^1.1.119",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
|
"@rushstack/eslint-patch": "^1.3.3",
|
||||||
|
"@types/har-format": "^1.2.12",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/lossless-json": "^1.0.1",
|
"@types/lossless-json": "^1.0.1",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/paho-mqtt": "^1.0.6",
|
"@types/paho-mqtt": "^1.0.7",
|
||||||
"@types/postman-collection": "^3.5.7",
|
"@types/postman-collection": "^3.5.7",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^9.0.2",
|
||||||
"@types/yargs-parser": "^21.0.0",
|
"@types/yargs-parser": "^21.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||||
"@typescript-eslint/parser": "^5.19.0",
|
"@typescript-eslint/parser": "^6.4.0",
|
||||||
"@vitejs/plugin-vue": "^3.1.0",
|
"@vitejs/plugin-vue": "^4.3.1",
|
||||||
"@vue/compiler-sfc": "^3.2.39",
|
"@vue/compiler-sfc": "^3.3.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.1",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/runtime-core": "^3.2.39",
|
"@vue/runtime-core": "^3.3.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.5.1",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"openapi-types": "^12.0.0",
|
"openapi-types": "^12.1.3",
|
||||||
"rollup-plugin-polyfill-node": "^0.10.1",
|
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||||
"sass": "^1.53.0",
|
"sass": "^1.66.0",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^5.1.6",
|
||||||
"unplugin-icons": "^0.14.9",
|
"unplugin-fonts": "^1.0.3",
|
||||||
"unplugin-vue-components": "^0.21.0",
|
"unplugin-icons": "^0.16.5",
|
||||||
"vite": "^3.1.4",
|
"unplugin-vue-components": "^0.25.1",
|
||||||
"vite-plugin-checker": "^0.5.1",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-fonts": "^0.6.0",
|
"vite-plugin-checker": "^0.6.1",
|
||||||
"vite-plugin-html-config": "^1.0.10",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.4",
|
"vite-plugin-inspect": "^0.7.38",
|
||||||
"vite-plugin-pages": "^0.26.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||||
"vite-plugin-pwa": "^0.13.1",
|
"vite-plugin-pwa": "^0.16.4",
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.8.0",
|
||||||
"vite-plugin-windicss": "^1.8.8",
|
"vite-plugin-windicss": "^1.9.1",
|
||||||
"vue-tsc": "^0.38.2",
|
"vitest": "^0.34.2",
|
||||||
|
"vue-tsc": "^1.8.8",
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/hoppscotch-common/public/badge.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 926 KiB |
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 535 KiB |
@@ -18,6 +18,7 @@ import { HOPP_MODULES } from "@modules/."
|
|||||||
import { isLoadingInitialRoute } from "@modules/router"
|
import { isLoadingInitialRoute } from "@modules/router"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
|
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
|
||||||
|
import { platform } from "./platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -45,4 +46,5 @@ if (APP_IS_IN_DEV_MODE) {
|
|||||||
|
|
||||||
// Run module root component setup code
|
// Run module root component setup code
|
||||||
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
|
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
|
||||||
|
platform.addedHoppModules?.forEach((mod) => mod.onRootSetup?.())
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
50
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,30 +1,37 @@
|
|||||||
// generated by unplugin-vue-components
|
/* eslint-disable */
|
||||||
// We suggest you to commit this file into source control
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
import '@vue/runtime-core'
|
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
|
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
AppFuse: typeof import('./components/app/Fuse.vue')['default']
|
|
||||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
|
AppInspection: typeof import('./components/app/Inspection.vue')['default']
|
||||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||||
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
||||||
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
|
|
||||||
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
|
|
||||||
AppShare: typeof import('./components/app/Share.vue')['default']
|
AppShare: typeof import('./components/app/Share.vue')['default']
|
||||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||||
|
AppSocial: typeof import('./components/app/Social.vue')['default']
|
||||||
|
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
|
||||||
|
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
||||||
|
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
||||||
|
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
|
||||||
|
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
|
||||||
|
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
||||||
|
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
|
||||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||||
@@ -53,6 +60,7 @@ declare module '@vue/runtime-core' {
|
|||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
|
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||||
@@ -65,12 +73,18 @@ declare module '@vue/runtime-core' {
|
|||||||
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
||||||
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
||||||
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
||||||
|
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
|
||||||
|
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
|
||||||
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
||||||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||||
|
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
|
||||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||||
|
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
|
||||||
|
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
|
||||||
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
||||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||||
|
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
|
||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
@@ -82,17 +96,22 @@ declare module '@vue/runtime-core' {
|
|||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||||
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||||
|
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
|
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
|
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||||
|
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
||||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
@@ -114,14 +133,17 @@ declare module '@vue/runtime-core' {
|
|||||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||||
HttpSidebar: typeof import('./components/http/Sidebar.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']
|
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
||||||
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
||||||
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
||||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
|
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
|
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
@@ -131,8 +153,11 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
|
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
@@ -152,6 +177,8 @@ declare module '@vue/runtime-core' {
|
|||||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
||||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
|
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||||
|
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
@@ -163,11 +190,13 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
||||||
|
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
|
||||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||||
|
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
|
||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
@@ -176,8 +205,8 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||||
SmartTree: typeof import('./components/smart/Tree.vue')['default']
|
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
||||||
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
|
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default']
|
||||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
||||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
||||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||||
@@ -193,5 +222,4 @@ declare module '@vue/runtime-core' {
|
|||||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,53 @@
|
|||||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||||
|
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmRemove"
|
||||||
|
:title="t('confirm.remove_team')"
|
||||||
|
@hide-modal="confirmRemove = false"
|
||||||
|
@resolve="deleteTeam()"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||||
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
const showShortcuts = ref(false)
|
const showShortcuts = ref(false)
|
||||||
const showShare = ref(false)
|
const showShare = ref(false)
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
|
|
||||||
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const teamID = ref<string | null>(null)
|
||||||
|
|
||||||
|
const deleteTeam = () => {
|
||||||
|
if (!teamID.value) return
|
||||||
|
pipe(
|
||||||
|
backendDeleteTeam(teamID.value),
|
||||||
|
TE.match(
|
||||||
|
(err) => {
|
||||||
|
// TODO: Better errors ? We know the possible errors now
|
||||||
|
toast.error(`${t("error.something_went_wrong")}`)
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
invokeAction("workspace.switch.personal")
|
||||||
|
toast.success(`${t("team.deleted")}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||||
|
}
|
||||||
|
|
||||||
defineActionHandler("flyouts.keybinds.toggle", () => {
|
defineActionHandler("flyouts.keybinds.toggle", () => {
|
||||||
showShortcuts.value = !showShortcuts.value
|
showShortcuts.value = !showShortcuts.value
|
||||||
})
|
})
|
||||||
@@ -23,4 +60,9 @@ defineActionHandler("modals.share.toggle", () => {
|
|||||||
defineActionHandler("modals.login.toggle", () => {
|
defineActionHandler("modals.login.toggle", () => {
|
||||||
showLogin.value = !showLogin.value
|
showLogin.value = !showLogin.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||||
|
teamID.value = teamId
|
||||||
|
confirmRemove.value = true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="contextMenuRef"
|
||||||
|
class="fixed bg-popover shadow-lg transform translate-y-8 border border-dividerDark p-2 rounded"
|
||||||
|
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
||||||
|
>
|
||||||
|
<div v-if="contextMenuOptions" class="flex flex-col">
|
||||||
|
<div
|
||||||
|
v-for="option in contextMenuOptions"
|
||||||
|
:key="option.id"
|
||||||
|
class="flex flex-col space-y-2"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="option.text.type === 'text' && option.text"
|
||||||
|
:icon="option.icon"
|
||||||
|
:label="option.text.text"
|
||||||
|
@click="handleClick(option)"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="option.text.component"
|
||||||
|
v-else-if="option.text.type === 'custom'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onClickOutside } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { ContextMenuResult, ContextMenuService } from "~/services/context-menu"
|
||||||
|
import { EnvironmentMenuService } from "~/services/context-menu/menu/environment.menu"
|
||||||
|
import { ParameterMenuService } from "~/services/context-menu/menu/parameter.menu"
|
||||||
|
import { URLMenuService } from "~/services/context-menu/menu/url.menu"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
position: { top: number; left: number }
|
||||||
|
text: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const contextMenuRef = ref<any | null>(null)
|
||||||
|
|
||||||
|
const contextMenuOptions = ref<ContextMenuResult[]>([])
|
||||||
|
|
||||||
|
onClickOutside(contextMenuRef, () => {
|
||||||
|
emit("hide-modal")
|
||||||
|
})
|
||||||
|
|
||||||
|
const contextMenuService = useService(ContextMenuService)
|
||||||
|
|
||||||
|
useService(EnvironmentMenuService)
|
||||||
|
useService(ParameterMenuService)
|
||||||
|
useService(URLMenuService)
|
||||||
|
|
||||||
|
const handleClick = (option: { action: () => void }) => {
|
||||||
|
option.action()
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.show, props.text],
|
||||||
|
(val) => {
|
||||||
|
if (val && props.text) {
|
||||||
|
const options = contextMenuService.getMenuFor(props.text)
|
||||||
|
contextMenuOptions.value = options
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -10,18 +10,6 @@
|
|||||||
:class="{ '-rotate-180': !EXPAND_NAVIGATION }"
|
:class="{ '-rotate-180': !EXPAND_NAVIGATION }"
|
||||||
@click="EXPAND_NAVIGATION = !EXPAND_NAVIGATION"
|
@click="EXPAND_NAVIGATION = !EXPAND_NAVIGATION"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="`${ZEN_MODE ? t('action.turn_off') : t('action.turn_on')} ${t(
|
|
||||||
'layout.zen_mode'
|
|
||||||
)}`"
|
|
||||||
:icon="ZEN_MODE ? IconMinimize : IconMaximize"
|
|
||||||
:class="{
|
|
||||||
'!text-accent !focus-visible:text-accentDark !hover:text-accentDark':
|
|
||||||
ZEN_MODE,
|
|
||||||
}"
|
|
||||||
@click="ZEN_MODE = !ZEN_MODE"
|
|
||||||
/>
|
|
||||||
<tippy interactive trigger="click" theme="popover">
|
<tippy interactive trigger="click" theme="popover">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -76,6 +64,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<!--
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="chat"
|
ref="chat"
|
||||||
:icon="IconMessageCircle"
|
:icon="IconMessageCircle"
|
||||||
@@ -88,20 +77,34 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
-->
|
||||||
:icon="IconGift"
|
<template
|
||||||
:label="`${t('app.whats_new')}`"
|
v-for="footerItem in platform.ui?.additionalFooterMenuItems"
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
:key="footerItem.id"
|
||||||
blank
|
>
|
||||||
@click="hide()"
|
<template v-if="footerItem.action.type === 'link'">
|
||||||
/>
|
<HoppSmartItem
|
||||||
<HoppSmartItem
|
:icon="footerItem.icon"
|
||||||
:icon="IconActivity"
|
:label="footerItem.text(t)"
|
||||||
:label="t('app.status')"
|
:to="footerItem.action.href"
|
||||||
to="https://status.hoppscotch.io"
|
blank
|
||||||
blank
|
@click="hide()"
|
||||||
@click="hide()"
|
/>
|
||||||
/>
|
</template>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-else
|
||||||
|
:icon="footerItem.icon"
|
||||||
|
:label="footerItem.text(t)"
|
||||||
|
blank
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
// @ts-expect-error TypeScript not understanding the type
|
||||||
|
footerItem.action.do()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<hr />
|
<hr />
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
:icon="IconGithub"
|
:icon="IconGithub"
|
||||||
@@ -152,7 +155,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${t(
|
:title="`${t(
|
||||||
'app.shortcuts'
|
'app.shortcuts'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>K</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>/</kbd>`"
|
||||||
:icon="IconZap"
|
:icon="IconZap"
|
||||||
@click="invokeAction('flyouts.keybinds.toggle')"
|
@click="invokeAction('flyouts.keybinds.toggle')"
|
||||||
/>
|
/>
|
||||||
@@ -196,26 +199,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue"
|
import { ref } from "vue"
|
||||||
import { version } from "~/../package.json"
|
import { version } from "~/../package.json"
|
||||||
import IconSidebar from "~icons/lucide/sidebar"
|
import IconSidebar from "~icons/lucide/sidebar"
|
||||||
import IconMinimize from "~icons/lucide/minimize"
|
|
||||||
import IconMaximize from "~icons/lucide/maximize"
|
|
||||||
import IconZap from "~icons/lucide/zap"
|
import IconZap from "~icons/lucide/zap"
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import IconColumns from "~icons/lucide/columns"
|
import IconColumns from "~icons/lucide/columns"
|
||||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||||
import IconShieldCheck from "~icons/lucide/shield-check"
|
import IconShieldCheck from "~icons/lucide/shield-check"
|
||||||
import IconBook from "~icons/lucide/book"
|
import IconBook from "~icons/lucide/book"
|
||||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
|
||||||
import IconGift from "~icons/lucide/gift"
|
|
||||||
import IconActivity from "~icons/lucide/activity"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
import IconGithub from "~icons/lucide/github"
|
||||||
import IconTwitter from "~icons/lucide/twitter"
|
import IconTwitter from "~icons/lucide/twitter"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconLock from "~icons/lucide/lock"
|
import IconLock from "~icons/lucide/lock"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
@@ -230,7 +227,6 @@ const showDeveloperOptions = ref(false)
|
|||||||
|
|
||||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||||
const SIDEBAR = useSetting("SIDEBAR")
|
const SIDEBAR = useSetting("SIDEBAR")
|
||||||
const ZEN_MODE = useSetting("ZEN_MODE")
|
|
||||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||||
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||||
|
|
||||||
@@ -241,13 +237,6 @@ const currentUser = useReadonlyStream(
|
|||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => ZEN_MODE.value,
|
|
||||||
() => {
|
|
||||||
EXPAND_NAVIGATION.value = !ZEN_MODE.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const nativeShare = () => {
|
const nativeShare = () => {
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
navigator
|
navigator
|
||||||
@@ -262,10 +251,6 @@ const nativeShare = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showDeveloperOptionModal = () => {
|
const showDeveloperOptionModal = () => {
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
showDeveloperOptions.value = true
|
showDeveloperOptions.value = true
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div key="outputHash" class="flex flex-col flex-1 overflow-auto">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<AppPowerSearchEntry
|
|
||||||
v-for="(shortcut, shortcutIndex) in searchResults"
|
|
||||||
:key="`shortcut-${shortcutIndex}`"
|
|
||||||
:active="shortcutIndex === selectedEntry"
|
|
||||||
:shortcut="shortcut.item"
|
|
||||||
@action="emit('action', shortcut.item.action)"
|
|
||||||
@mouseover="selectedEntry = shortcutIndex"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="searchResults.length === 0"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
|
||||||
<span class="my-2 text-center">
|
|
||||||
{{ t("state.nothing_found") }} "{{ search }}"
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onUnmounted, onMounted } from "vue"
|
|
||||||
import Fuse from "fuse.js"
|
|
||||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
|
||||||
import { HoppAction } from "~/helpers/actions"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
input: Record<string, any>[]
|
|
||||||
search: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "action", action: HoppAction): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
keys: ["keys", "label", "action", "tags"],
|
|
||||||
}
|
|
||||||
|
|
||||||
const fuse = new Fuse(props.input, options)
|
|
||||||
|
|
||||||
const searchResults = computed(() => fuse.search(props.search))
|
|
||||||
|
|
||||||
const searchResultsItems = computed(() =>
|
|
||||||
searchResults.value.map((searchResult) => searchResult.item)
|
|
||||||
)
|
|
||||||
|
|
||||||
const emitSearchAction = (action: HoppAction) => emit("action", action)
|
|
||||||
|
|
||||||
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
|
||||||
useArrowKeysNavigation(searchResultsItems, {
|
|
||||||
onEnter: emitSearchAction,
|
|
||||||
stopPropagation: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
bindArrowKeysListeners()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unbindArrowKeysListeners()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -15,16 +15,21 @@
|
|||||||
:label="t('app.name')"
|
:label="t('app.name')"
|
||||||
to="/"
|
to="/"
|
||||||
/>
|
/>
|
||||||
<!-- <AppGitHubStarButton class="mt-1.5 transition" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center space-x-2">
|
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
||||||
<HoppButtonSecondary
|
<button
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
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"
|
||||||
:title="`${t('app.search')} <kbd>/</kbd>`"
|
|
||||||
:icon="IconSearch"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@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">
|
||||||
|
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="showInstallButton"
|
v-if="showInstallButton"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -42,6 +47,8 @@
|
|||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.support.toggle')"
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
||||||
<div
|
<div
|
||||||
v-if="currentUser === null"
|
v-if="currentUser === null"
|
||||||
class="inline-flex items-center space-x-2"
|
class="inline-flex items-center space-x-2"
|
||||||
@@ -236,19 +243,21 @@ import IconDownload from "~icons/lucide/download"
|
|||||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import IconSearch from "~icons/lucide/search"
|
|
||||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { invokeAction } from "@helpers/actions"
|
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once the PWA code is initialized, this holds a method
|
* Once the PWA code is initialized, this holds a method
|
||||||
@@ -365,6 +374,8 @@ const handleTeamEdit = () => {
|
|||||||
editingTeamID.value = workspace.value.teamID
|
editingTeamID.value = workspace.value.teamID
|
||||||
editingTeamName.value = { name: selectedTeam.value.name }
|
editingTeamName.value = { name: selectedTeam.value.name }
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
|
} else {
|
||||||
|
noPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,4 +385,29 @@ const profile = ref<any | null>(null)
|
|||||||
const settings = ref<any | null>(null)
|
const settings = ref<any | null>(null)
|
||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
|
||||||
|
defineActionHandler("modals.team.edit", handleTeamEdit)
|
||||||
|
|
||||||
|
defineActionHandler("modals.team.invite", () => {
|
||||||
|
if (
|
||||||
|
selectedTeam.value?.myRole === "OWNER" ||
|
||||||
|
selectedTeam.value?.myRole === "EDITOR"
|
||||||
|
) {
|
||||||
|
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
|
||||||
|
} else {
|
||||||
|
noPermission()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineActionHandler(
|
||||||
|
"user.login",
|
||||||
|
() => {
|
||||||
|
invokeAction("modals.login.toggle")
|
||||||
|
},
|
||||||
|
computed(() => !currentUser.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const noPermission = () => {
|
||||||
|
toast.error(`${t("profile.no_permission")}`)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="inspectionResults && inspectionResults.length > 0">
|
||||||
|
<tippy interactive trigger="click" theme="popover">
|
||||||
|
<div class="flex justify-center items-center flex-1 flex-col">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconAlertTriangle"
|
||||||
|
:class="severityColor(getHighestSeverity.severity)"
|
||||||
|
:title="t('inspections.description')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div class="flex flex-col space-y-2 items-start flex-1">
|
||||||
|
<div
|
||||||
|
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
|
||||||
|
>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ t("inspections.title") }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(inspector, index) in inspectionResults"
|
||||||
|
:key="index"
|
||||||
|
class="flex self-stretch max-w-md w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="inspector.text.type === 'text'"
|
||||||
|
class="flex-1 px-3 py-2"
|
||||||
|
>
|
||||||
|
{{ inspector.text.text }}
|
||||||
|
<HoppSmartLink
|
||||||
|
blank
|
||||||
|
:to="inspector.doc.link"
|
||||||
|
class="text-accent hover:text-accentDark transition"
|
||||||
|
>
|
||||||
|
{{ inspector.doc.text }}
|
||||||
|
<icon-lucide-arrow-up-right class="svg-icons" />
|
||||||
|
</HoppSmartLink>
|
||||||
|
</span>
|
||||||
|
<span v-if="inspector.action" class="flex p-2 space-x-2">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="inspector.action.text"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
inspector.action?.apply()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { InspectorResult } from "~/services/inspection"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
inspectionResults: InspectorResult[] | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const getHighestSeverity = computed(() => {
|
||||||
|
if (props.inspectionResults) {
|
||||||
|
return props.inspectionResults.reduce(
|
||||||
|
(prev, curr) => {
|
||||||
|
return prev.severity > curr.severity ? prev : curr
|
||||||
|
},
|
||||||
|
{ severity: 0 }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return { severity: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const severityColor = (severity: number) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 1:
|
||||||
|
return "!text-green-500 hover:!text-green-600"
|
||||||
|
case 2:
|
||||||
|
return "!text-yellow-500 hover:!text-yellow-600"
|
||||||
|
case 3:
|
||||||
|
return "!text-red-500 hover:!text-red-600"
|
||||||
|
default:
|
||||||
|
return "!text-gray-500 hover:!text-gray-600"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,91 +8,41 @@
|
|||||||
{{ t("settings.interceptor_description") }}
|
{{ t("settings.interceptor_description") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartRadioGroup
|
|
||||||
v-model="interceptorSelection"
|
<div>
|
||||||
:radios="interceptors"
|
<div
|
||||||
/>
|
v-for="interceptor in interceptors"
|
||||||
<div
|
:key="interceptor.interceptorID"
|
||||||
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
class="flex flex-col"
|
||||||
class="flex space-x-2"
|
>
|
||||||
>
|
<HoppSmartRadio
|
||||||
<HoppButtonSecondary
|
:value="interceptor.interceptorID"
|
||||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
:label="unref(interceptor.name(t))"
|
||||||
blank
|
:selected="interceptorSelection === interceptor.interceptorID"
|
||||||
:icon="IconChrome"
|
@change="interceptorSelection = interceptor.interceptorID"
|
||||||
label="Chrome"
|
/>
|
||||||
outline
|
|
||||||
class="!flex-1"
|
<component
|
||||||
/>
|
:is="interceptor.selectorSubtitle"
|
||||||
<HoppButtonSecondary
|
v-if="interceptor.selectorSubtitle"
|
||||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
/>
|
||||||
blank
|
</div>
|
||||||
:icon="IconFirefox"
|
|
||||||
label="Firefox"
|
|
||||||
outline
|
|
||||||
class="!flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconChrome from "~icons/brands/chrome"
|
|
||||||
import IconFirefox from "~icons/brands/firefox"
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { applySetting, toggleSetting } from "~/newstore/settings"
|
|
||||||
import { useSetting } from "@composables/settings"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useService } from "dioc/vue"
|
||||||
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
import { Ref, unref } from "vue"
|
||||||
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
const interceptorService = useService(InterceptorService)
|
||||||
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
|
||||||
|
|
||||||
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
const interceptorSelection =
|
||||||
|
interceptorService.currentInterceptorID as Ref<string>
|
||||||
|
|
||||||
const extensionVersion = computed(() => {
|
const interceptors = interceptorService.availableInterceptors
|
||||||
return currentExtensionStatus.value === "available"
|
|
||||||
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
|
|
||||||
: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const interceptors = computed(() => [
|
|
||||||
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
|
|
||||||
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
|
|
||||||
{
|
|
||||||
value: "EXTENSIONS_ENABLED" as const,
|
|
||||||
label:
|
|
||||||
`${t("settings.extensions")}: ` +
|
|
||||||
(extensionVersion.value !== null
|
|
||||||
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
|
|
||||||
: t("settings.extension_ver_not_reported")),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
|
|
||||||
|
|
||||||
const interceptorSelection = computed<InterceptorMode>({
|
|
||||||
get() {
|
|
||||||
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
|
|
||||||
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
|
|
||||||
return "BROWSER_ENABLED"
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
if (val === "EXTENSIONS_ENABLED") {
|
|
||||||
applySetting("EXTENSIONS_ENABLED", true)
|
|
||||||
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
|
|
||||||
}
|
|
||||||
if (val === "PROXY_ENABLED") {
|
|
||||||
applySetting("PROXY_ENABLED", true)
|
|
||||||
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
|
|
||||||
}
|
|
||||||
if (val === "BROWSER_ENABLED") {
|
|
||||||
applySetting("PROXY_ENABLED", false)
|
|
||||||
applySetting("EXTENSIONS_ENABLED", false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,163 +30,63 @@
|
|||||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||||
{{ t("support.title") }}
|
{{ t("support.title") }}
|
||||||
</h2>
|
</h2>
|
||||||
<HoppSmartItem
|
<template
|
||||||
:icon="IconBook"
|
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||||
:label="t('app.documentation')"
|
:key="item.id"
|
||||||
to="https://docs.hoppscotch.io"
|
>
|
||||||
:description="t('support.documentation')"
|
<HoppSmartItem
|
||||||
:info-icon="IconChevronRight"
|
v-if="item.action.type === 'link'"
|
||||||
active
|
:icon="item.icon"
|
||||||
blank
|
:label="item.text(t)"
|
||||||
@click="hideModal()"
|
:to="item.action.href"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconGift"
|
active
|
||||||
:label="t('app.whats_new')"
|
blank
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
@click="hideModal()"
|
||||||
:description="t('support.changelog')"
|
/>
|
||||||
:info-icon="IconChevronRight"
|
<HoppSmartItem
|
||||||
active
|
v-else
|
||||||
blank
|
:icon="item.icon"
|
||||||
@click="hideModal()"
|
:label="item.text(t)"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconActivity"
|
active
|
||||||
:label="t('app.status')"
|
@click="
|
||||||
to="https://status.hoppscotch.io"
|
() => {
|
||||||
blank
|
// @ts-expect-error Typescript isn't able to understand
|
||||||
:description="t('app.status_description')"
|
item.action.do()
|
||||||
:info-icon="IconChevronRight"
|
hideModal()
|
||||||
active
|
}
|
||||||
@click="hideModal()"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
</template>
|
||||||
:icon="IconLock"
|
|
||||||
:label="`${t('app.terms_and_privacy')}`"
|
|
||||||
to="https://docs.hoppscotch.io/support/privacy"
|
|
||||||
blank
|
|
||||||
:description="t('app.terms_and_privacy')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
|
||||||
{{ t("settings.follow") }}
|
|
||||||
</h2>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconDiscord"
|
|
||||||
:label="t('app.discord')"
|
|
||||||
to="https://hoppscotch.io/discord"
|
|
||||||
blank
|
|
||||||
:description="t('app.join_discord_community')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconTwitter"
|
|
||||||
:label="t('app.twitter')"
|
|
||||||
to="https://hoppscotch.io/twitter"
|
|
||||||
blank
|
|
||||||
:description="t('support.twitter')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="`${t('app.github')}`"
|
|
||||||
to="https://github.com/hoppscotch/hoppscotch"
|
|
||||||
blank
|
|
||||||
:description="t('support.github')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconMessageCircle"
|
|
||||||
:label="t('app.chat_with_us')"
|
|
||||||
:description="t('support.chat')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="chatWithUs()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconUserPlus"
|
|
||||||
:label="`${t('app.invite')}`"
|
|
||||||
:description="t('shortcut.miscellaneous.invite')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="expandInvite()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-if="navigatorShare"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconShare2"
|
|
||||||
:label="`${t('request.share')}`"
|
|
||||||
:description="t('request.share_description')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="nativeShare()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue"
|
|
||||||
import IconSidebar from "~icons/lucide/sidebar"
|
import IconSidebar from "~icons/lucide/sidebar"
|
||||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||||
import IconBook from "~icons/lucide/book"
|
|
||||||
import IconGift from "~icons/lucide/gift"
|
|
||||||
import IconActivity from "~icons/lucide/activity"
|
|
||||||
import IconLock from "~icons/lucide/lock"
|
|
||||||
import IconDiscord from "~icons/brands/discord"
|
|
||||||
import IconTwitter from "~icons/brands/twitter"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
|
||||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
|
||||||
import IconChevronRight from "~icons/lucide/chevron-right"
|
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const navigatorShare = !!navigator.share
|
|
||||||
const showShare = ref(false)
|
|
||||||
|
|
||||||
const ZEN_MODE = useSetting("ZEN_MODE")
|
|
||||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||||
const SIDEBAR = useSetting("SIDEBAR")
|
const SIDEBAR = useSetting("SIDEBAR")
|
||||||
|
|
||||||
watch(
|
|
||||||
() => ZEN_MODE.value,
|
|
||||||
() => {
|
|
||||||
EXPAND_NAVIGATION.value = !ZEN_MODE.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineActionHandler("modals.share.toggle", () => {
|
|
||||||
showShare.value = !showShare.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandNavigation = () => {
|
const expandNavigation = () => {
|
||||||
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
|
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
|
||||||
hideModal()
|
hideModal()
|
||||||
@@ -197,24 +97,6 @@ const expandCollection = () => {
|
|||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandInvite = () => {
|
|
||||||
showShare.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const nativeShare = () => {
|
|
||||||
if (navigator.share) {
|
|
||||||
navigator
|
|
||||||
.share({
|
|
||||||
title: "Hoppscotch",
|
|
||||||
text: "Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.",
|
|
||||||
url: "https://hoppscotch.io",
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
} else {
|
|
||||||
// fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,18 @@
|
|||||||
:horizontal="COLUMN_LAYOUT"
|
:horizontal="COLUMN_LAYOUT"
|
||||||
@resize="setPaneEvent($event, 'horizontal')"
|
@resize="setPaneEvent($event, 'horizontal')"
|
||||||
>
|
>
|
||||||
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
|
<Pane
|
||||||
|
:size="PANE_MAIN_TOP_SIZE"
|
||||||
|
class="flex flex-col !overflow-auto"
|
||||||
|
min-size="25"
|
||||||
|
>
|
||||||
<slot name="primary" />
|
<slot name="primary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane
|
<Pane
|
||||||
v-if="hasSecondary"
|
v-if="hasSecondary"
|
||||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||||
class="flex flex-col !overflow-auto"
|
class="flex flex-col !overflow-auto"
|
||||||
|
min-size="25"
|
||||||
>
|
>
|
||||||
<slot name="secondary" />
|
<slot name="secondary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
@@ -33,7 +38,7 @@
|
|||||||
<Pane
|
<Pane
|
||||||
v-if="SIDEBAR && hasSidebar"
|
v-if="SIDEBAR && hasSidebar"
|
||||||
:size="PANE_SIDEBAR_SIZE"
|
:size="PANE_SIDEBAR_SIZE"
|
||||||
min-size="20"
|
min-size="25"
|
||||||
class="flex flex-col !overflow-auto bg-primaryContrast"
|
class="flex flex-col !overflow-auto bg-primaryContrast"
|
||||||
>
|
>
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
@@ -78,10 +83,10 @@ type PaneEvent = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANE_MAIN_SIZE = ref(74)
|
const PANE_MAIN_SIZE = ref(70)
|
||||||
const PANE_SIDEBAR_SIZE = ref(26)
|
const PANE_SIDEBAR_SIZE = ref(30)
|
||||||
const PANE_MAIN_TOP_SIZE = ref(42)
|
const PANE_MAIN_TOP_SIZE = ref(35)
|
||||||
const PANE_MAIN_BOTTOM_SIZE = ref(58)
|
const PANE_MAIN_BOTTOM_SIZE = ref(65)
|
||||||
|
|
||||||
if (!COLUMN_LAYOUT.value) {
|
if (!COLUMN_LAYOUT.value) {
|
||||||
PANE_MAIN_TOP_SIZE.value = 50
|
PANE_MAIN_TOP_SIZE.value = 50
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<HoppSmartModal
|
|
||||||
v-if="show"
|
|
||||||
styles="sm:max-w-lg"
|
|
||||||
full-width
|
|
||||||
@close="emit('hide-modal')"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<div class="flex flex-col border-b transition border-dividerLight">
|
|
||||||
<input
|
|
||||||
id="command"
|
|
||||||
v-model="search"
|
|
||||||
v-focus
|
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
|
||||||
name="command"
|
|
||||||
:placeholder="`${t('app.type_a_command_search')}`"
|
|
||||||
class="flex flex-shrink-0 p-6 text-base bg-transparent text-secondaryDark"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<kbd class="shortcut-key">↑</kbd>
|
|
||||||
<kbd class="shortcut-key">↓</kbd>
|
|
||||||
<span class="ml-2 truncate">
|
|
||||||
{{ t("action.to_navigate") }}
|
|
||||||
</span>
|
|
||||||
<kbd class="shortcut-key">↩</kbd>
|
|
||||||
<span class="ml-2 truncate">
|
|
||||||
{{ t("action.to_select") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<kbd class="shortcut-key">ESC</kbd>
|
|
||||||
<span class="ml-2 truncate">
|
|
||||||
{{ t("action.to_close") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AppFuse
|
|
||||||
v-if="search && show"
|
|
||||||
:input="fuse"
|
|
||||||
:search="search"
|
|
||||||
@action="runAction"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(map, mapIndex) in mappings"
|
|
||||||
:key="`map-${mapIndex}`"
|
|
||||||
class="flex flex-col"
|
|
||||||
>
|
|
||||||
<h5 class="px-6 py-2 my-2 text-secondaryLight">
|
|
||||||
{{ t(map.section) }}
|
|
||||||
</h5>
|
|
||||||
<AppPowerSearchEntry
|
|
||||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
|
||||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
|
||||||
:shortcut="shortcut"
|
|
||||||
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
|
|
||||||
@action="runAction"
|
|
||||||
@mouseover="selectedEntry = shortcutsItems.indexOf(shortcut)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch } from "vue"
|
|
||||||
import { HoppAction, invokeAction } from "~/helpers/actions"
|
|
||||||
import { spotlight as mappings, fuse } from "@helpers/shortcuts"
|
|
||||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const search = ref("")
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
search.value = ""
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const runAction = (command: HoppAction) => {
|
|
||||||
invokeAction(command)
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcutsItems = computed(() =>
|
|
||||||
mappings.reduce(
|
|
||||||
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
|
||||||
useArrowKeysNavigation(shortcutsItems, {
|
|
||||||
onEnter: runAction,
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(show) => {
|
|
||||||
if (show) bindArrowKeysListeners()
|
|
||||||
else unbindArrowKeysListeners()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
|
|
||||||
:class="{ active: active }"
|
|
||||||
tabindex="-1"
|
|
||||||
@click="emit('action', shortcut.action)"
|
|
||||||
@keydown.enter="emit('action', shortcut.action)"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="shortcut.icon"
|
|
||||||
class="mr-4 transition opacity-50 svg-icons"
|
|
||||||
:class="{ 'opacity-100 text-secondaryDark': active }"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="flex flex-1 mr-4 transition"
|
|
||||||
:class="{ 'text-secondaryDark': active }"
|
|
||||||
>
|
|
||||||
{{ t(shortcut.label) }}
|
|
||||||
</span>
|
|
||||||
<kbd
|
|
||||||
v-for="(key, keyIndex) in shortcut.keys"
|
|
||||||
:key="`key-${String(keyIndex)}`"
|
|
||||||
class="shortcut-key"
|
|
||||||
>
|
|
||||||
{{ key }}
|
|
||||||
</kbd>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Component } from "vue"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
shortcut: {
|
|
||||||
label: string
|
|
||||||
keys: string[]
|
|
||||||
action: string
|
|
||||||
icon: object | Component
|
|
||||||
}
|
|
||||||
active: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "action", action: string): void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.search-entry {
|
|
||||||
@apply relative;
|
|
||||||
@apply after:absolute;
|
|
||||||
@apply after:top-0;
|
|
||||||
@apply after:left-0;
|
|
||||||
@apply after:bottom-0;
|
|
||||||
@apply after:bg-transparent;
|
|
||||||
@apply after:z-2;
|
|
||||||
@apply after:w-0.5;
|
|
||||||
@apply after:content-DEFAULT;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply bg-primaryLight;
|
|
||||||
@apply after:bg-accentLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,56 +4,26 @@
|
|||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
|
<HoppSmartInput
|
||||||
<input
|
v-model="filterText"
|
||||||
v-model="filterText"
|
type="search"
|
||||||
type="search"
|
styles="px-6 py-4 border-b border-dividerLight"
|
||||||
autocomplete="off"
|
:placeholder="`${t('action.search')}`"
|
||||||
class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
input-styles="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>
|
||||||
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
|
<div class="flex flex-col divide-y divide-dividerLight">
|
||||||
<details
|
<HoppSmartPlaceholder
|
||||||
v-for="(map, mapIndex) in searchResults"
|
v-if="isEmpty(shortcutsResults)"
|
||||||
:key="`map-${mapIndex}`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
class="flex flex-col"
|
|
||||||
open
|
|
||||||
>
|
|
||||||
<summary
|
|
||||||
class="flex items-center flex-1 min-w-0 px-6 py-4 font-semibold transition cursor-pointer focus:outline-none text-secondaryLight hover:text-secondaryDark"
|
|
||||||
>
|
|
||||||
<icon-lucide-chevron-right class="mr-2 indicator" />
|
|
||||||
<span
|
|
||||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
|
||||||
>
|
|
||||||
{{ t(map.item.section) }}
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
|
||||||
<AppShortcutsEntry
|
|
||||||
v-for="(shortcut, index) in map.item.shortcuts"
|
|
||||||
:key="`shortcut-${index}`"
|
|
||||||
:shortcut="shortcut"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<div
|
|
||||||
v-if="searchResults.length === 0"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<span class="my-2 text-center flex flex-col">
|
</HoppSmartPlaceholder>
|
||||||
{{ t("state.nothing_found") }}
|
|
||||||
<span class="break-all">"{{ filterText }}"</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col divide-y divide-dividerLight">
|
|
||||||
<details
|
<details
|
||||||
v-for="(map, mapIndex) in mappings"
|
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||||
:key="`map-${mapIndex}`"
|
v-else
|
||||||
|
:key="`section-${sectionTitle}`"
|
||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
@@ -64,13 +34,13 @@
|
|||||||
<span
|
<span
|
||||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||||
>
|
>
|
||||||
{{ t(map.section) }}
|
{{ sectionTitle }}
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||||
<AppShortcutsEntry
|
<AppShortcutsEntry
|
||||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
v-for="(shortcut, index) in sectionResults"
|
||||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
:key="`shortcut-${index}`"
|
||||||
:shortcut="shortcut"
|
:shortcut="shortcut"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,10 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue"
|
import { computed, onBeforeMount, ref } from "vue"
|
||||||
import Fuse from "fuse.js"
|
import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
|
||||||
import mappings from "~/helpers/shortcuts"
|
import MiniSearch from "minisearch"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { groupBy, isEmpty } from "lodash-es"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -92,15 +63,33 @@ defineProps<{
|
|||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const options = {
|
const minisearch = new MiniSearch({
|
||||||
keys: ["shortcuts.label"],
|
fields: ["label", "keys", "section"],
|
||||||
}
|
idField: "label",
|
||||||
|
storeFields: ["label", "keys", "section"],
|
||||||
|
searchOptions: {
|
||||||
|
fuzzy: true,
|
||||||
|
prefix: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const fuse = new Fuse(mappings, options)
|
const shortcuts = getShortcuts(t)
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
minisearch.addAllAsync(shortcuts)
|
||||||
|
})
|
||||||
|
|
||||||
const filterText = ref("")
|
const filterText = ref("")
|
||||||
|
|
||||||
const searchResults = computed(() => fuse.search(filterText.value))
|
const shortcutsResults = computed(() => {
|
||||||
|
// If there are no search text, return all the shortcuts
|
||||||
|
const results =
|
||||||
|
filterText.value.length > 0
|
||||||
|
? minisearch.search(filterText.value)
|
||||||
|
: shortcuts
|
||||||
|
|
||||||
|
return groupBy(results, "section") as Record<string, ShortcutDef[]>
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void
|
(e: "close"): void
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center py-1">
|
<div class="flex items-center py-1">
|
||||||
<span class="flex flex-1 mr-4">
|
<span class="flex flex-1 mr-4">
|
||||||
{{ t(shortcut.label) }}
|
{{ shortcut.label }}
|
||||||
</span>
|
</span>
|
||||||
<kbd
|
<kbd
|
||||||
v-for="(key, index) in shortcut.keys"
|
v-for="(key, index) in shortcut.keys"
|
||||||
@@ -14,14 +14,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { ShortcutDef } from "~/helpers/shortcuts"
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
shortcut: {
|
shortcut: ShortcutDef
|
||||||
label: string
|
|
||||||
keys: string[]
|
|
||||||
}
|
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||||
<div class="flex pb-4 my-4 space-x-2">
|
<div class="flex mb-4 space-x-2">
|
||||||
<div class="flex flex-col items-end space-y-4 text-right">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex items-center flex-1">
|
||||||
{{ t("shortcut.request.send_request") }}
|
{{ t("shortcut.request.send_request") }}
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
<kbd class="shortcut-key">K</kbd>
|
<kbd class="shortcut-key">/</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">/</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">?</kbd>
|
<kbd class="shortcut-key">?</kbd>
|
||||||
|
|||||||
@@ -8,89 +8,46 @@
|
|||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<HoppSmartItem
|
<template
|
||||||
:icon="IconBook"
|
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||||
:label="t('app.documentation')"
|
:key="item.id"
|
||||||
to="https://docs.hoppscotch.io"
|
>
|
||||||
:description="t('support.documentation')"
|
<HoppSmartItem
|
||||||
:info-icon="IconChevronRight"
|
v-if="item.action.type === 'link'"
|
||||||
active
|
:icon="item.icon"
|
||||||
blank
|
:label="item.text(t)"
|
||||||
@click="hideModal()"
|
:to="item.action.href"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconZap"
|
active
|
||||||
:label="t('app.keyboard_shortcuts')"
|
blank
|
||||||
:description="t('support.shortcuts')"
|
@click="hideModal()"
|
||||||
:info-icon="IconChevronRight"
|
/>
|
||||||
active
|
<HoppSmartItem
|
||||||
@click="showShortcuts()"
|
v-else
|
||||||
/>
|
:icon="item.icon"
|
||||||
<HoppSmartItem
|
:label="item.text(t)"
|
||||||
:icon="IconGift"
|
:description="item.subtitle(t)"
|
||||||
:label="t('app.whats_new')"
|
:info-icon="IconChevronRight"
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
active
|
||||||
:description="t('support.changelog')"
|
@click="
|
||||||
:info-icon="IconChevronRight"
|
() => {
|
||||||
active
|
// @ts-expect-error Typescript isn't able to understand
|
||||||
blank
|
item.action.do()
|
||||||
@click="hideModal()"
|
hideModal()
|
||||||
/>
|
}
|
||||||
<HoppSmartItem
|
"
|
||||||
:icon="IconMessageCircle"
|
/>
|
||||||
:label="t('app.chat_with_us')"
|
</template>
|
||||||
:description="t('support.chat')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="chatWithUs()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGitHub"
|
|
||||||
:label="t('app.github')"
|
|
||||||
to="https://hoppscotch.io/github"
|
|
||||||
blank
|
|
||||||
:description="t('support.github')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconDiscord"
|
|
||||||
:label="t('app.join_discord_community')"
|
|
||||||
to="https://hoppscotch.io/discord"
|
|
||||||
blank
|
|
||||||
:description="t('support.community')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconTwitter"
|
|
||||||
:label="t('app.twitter')"
|
|
||||||
to="https://hoppscotch.io/twitter"
|
|
||||||
blank
|
|
||||||
:description="t('support.twitter')"
|
|
||||||
:info-icon="IconChevronRight"
|
|
||||||
active
|
|
||||||
@click="hideModal()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconTwitter from "~icons/brands/twitter"
|
|
||||||
import IconDiscord from "~icons/brands/discord"
|
|
||||||
import IconGitHub from "~icons/lucide/github"
|
|
||||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
|
||||||
import IconGift from "~icons/lucide/gift"
|
|
||||||
import IconZap from "~icons/lucide/zap"
|
|
||||||
import IconBook from "~icons/lucide/book"
|
|
||||||
import IconChevronRight from "~icons/lucide/chevron-right"
|
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import { invokeAction } from "@helpers/actions"
|
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -102,16 +59,6 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showShortcuts = () => {
|
|
||||||
invokeAction("flyouts.keybinds.toggle")
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
ref="el"
|
||||||
|
class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
|
||||||
|
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="emit('action')"
|
||||||
|
@keydown.enter="emit('action')"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="entry.icon"
|
||||||
|
class="opacity-50 svg-icons"
|
||||||
|
:class="{ 'opacity-100': active }"
|
||||||
|
/>
|
||||||
|
<template
|
||||||
|
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
||||||
|
>
|
||||||
|
<span class="block truncate">
|
||||||
|
{{ entry.text.text }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-else-if="entry.text.type === 'text' && Array.isArray(entry.text.text)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(labelPart, labelPartIndex) in entry.text.text"
|
||||||
|
:key="`label-${labelPart}-${labelPartIndex}`"
|
||||||
|
>
|
||||||
|
<span class="block truncate">
|
||||||
|
{{ labelPart }}
|
||||||
|
</span>
|
||||||
|
<icon-lucide-chevron-right
|
||||||
|
v-if="labelPartIndex < entry.text.text.length - 1"
|
||||||
|
class="flex flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="entry.text.type === 'custom'">
|
||||||
|
<span class="block truncate">
|
||||||
|
<component
|
||||||
|
:is="entry.text.component"
|
||||||
|
v-bind="entry.text.componentProps"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-if="formattedShortcutKeys" class="block truncate">
|
||||||
|
<kbd
|
||||||
|
v-for="(key, keyIndex) in formattedShortcutKeys"
|
||||||
|
:key="`key-${String(keyIndex)}`"
|
||||||
|
class="shortcut-key"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</kbd>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { SpotlightSearcherResult } from "~/services/spotlight"
|
||||||
|
|
||||||
|
const SPECIAL_KEY_CHARS: Record<string, string> = {
|
||||||
|
ctrl: getPlatformSpecialKey(),
|
||||||
|
alt: getPlatformAlternateKey(),
|
||||||
|
up: "↑",
|
||||||
|
down: "↓",
|
||||||
|
enter: "↩",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch, ref } from "vue"
|
||||||
|
import { capitalize } from "lodash-es"
|
||||||
|
import { getPlatformAlternateKey } from "~/helpers/platformutils"
|
||||||
|
|
||||||
|
const el = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
entry: SpotlightSearcherResult
|
||||||
|
active: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formattedShortcutKeys = computed(
|
||||||
|
() =>
|
||||||
|
props.entry.meta?.keyboardShortcut?.map((key) => {
|
||||||
|
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "action"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.active,
|
||||||
|
(active) => {
|
||||||
|
if (active) {
|
||||||
|
el.value?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "nearest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-entry {
|
||||||
|
@apply after:absolute;
|
||||||
|
@apply after:top-0;
|
||||||
|
@apply after:left-0;
|
||||||
|
@apply after:bottom-0;
|
||||||
|
@apply after:bg-transparent;
|
||||||
|
@apply after:z-2;
|
||||||
|
@apply after:w-0.5;
|
||||||
|
@apply after:content-DEFAULT;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply after:bg-accentLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll-padding: 4rem !important;
|
||||||
|
scroll-margin: 4rem !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||