Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeee8af806 | ||
|
|
134441a6e7 | ||
|
|
80a5d21576 | ||
|
|
f1a812dae2 | ||
|
|
65a194a6d2 | ||
|
|
55e3dd3c18 | ||
|
|
b88f496f4e | ||
|
|
8caf9f110b | ||
|
|
1370b53726 | ||
|
|
8590a9a110 | ||
|
|
62058d5dfe | ||
|
|
1d397af674 | ||
|
|
141a468808 | ||
|
|
a24d724e2b | ||
|
|
dd72eacd21 | ||
|
|
c3c3fc6720 | ||
|
|
37a3b72025 | ||
|
|
defece95fc | ||
|
|
dbb45e7253 | ||
|
|
cc802b1e9f | ||
|
|
a66a2f5645 | ||
|
|
39afeab5f8 | ||
|
|
1583c86c78 | ||
|
|
1372681b87 | ||
|
|
2179ce6fff | ||
|
|
7e1b26c6a9 | ||
|
|
ae9b7183b5 | ||
|
|
3fa4052538 | ||
|
|
f2de0dc673 | ||
|
|
7e686a8882 | ||
|
|
4ca6e9ec3a | ||
|
|
dcd441f15e | ||
|
|
90c8fbeee4 | ||
|
|
cae1840506 | ||
|
|
82c6f6f6bc | ||
|
|
2545262fc2 | ||
|
|
b27fe871c4 | ||
|
|
cb5fff0310 | ||
|
|
d15caba4a6 | ||
|
|
536c8128dd | ||
|
|
99918ee0c0 | ||
|
|
864d40d934 | ||
|
|
a227af05d9 | ||
|
|
ce0898956d | ||
|
|
cd72851289 | ||
|
|
f676f94278 | ||
|
|
cd6e40f01c | ||
|
|
59a8a22e8a | ||
|
|
2910164d5a | ||
|
|
b95e2b365a | ||
|
|
73e788b513 | ||
|
|
15d135c11b | ||
|
|
0fcda0be1a | ||
|
|
9d7052c626 | ||
|
|
5841d2eb66 | ||
|
|
ee07a90b5e | ||
|
|
70d2f1e3d9 | ||
|
|
acafc072db | ||
|
|
51e40581b0 | ||
|
|
1e5dd1cc53 | ||
|
|
3d7b057026 | ||
|
|
d36ab337d7 | ||
|
|
012f9b5314 | ||
|
|
ba6069324f | ||
|
|
0d26d4cdbd | ||
|
|
4b920feffa | ||
|
|
830373efb3 | ||
|
|
c3f18671ec | ||
|
|
0d33758ba4 | ||
|
|
e7e8c397ef | ||
|
|
b04b12c7a0 | ||
|
|
a1d69b3210 | ||
|
|
dcbc2f1145 | ||
|
|
36903b338a | ||
|
|
9d8d6832af | ||
|
|
3d004f2322 | ||
|
|
fb827e3586 | ||
|
|
ccca183e08 | ||
|
|
237455ab21 | ||
|
|
6141073137 | ||
|
|
740691417f | ||
|
|
2ed709796a | ||
|
|
75c0350584 | ||
|
|
17d72b9922 | ||
|
|
1796fae3d1 | ||
|
|
1ecd22204d | ||
|
|
356fe4591f | ||
|
|
0230942a3d | ||
|
|
325793eebc | ||
|
|
39d1256f68 | ||
|
|
fd2472d34b | ||
|
|
5e8fbc6552 | ||
|
|
3084a40729 | ||
|
|
0069f51ea4 | ||
|
|
696c612489 | ||
|
|
4a5a4077af | ||
|
|
28bcb899e7 | ||
|
|
4062a7089a | ||
|
|
ad86221c7d | ||
|
|
53938248de | ||
|
|
c018b639ad | ||
|
|
eb2145c7da | ||
|
|
2f4c39d310 | ||
|
|
79ada82223 | ||
|
|
c67463fb3b | ||
|
|
d162471555 | ||
|
|
c99797bcef | ||
|
|
9739cdbbaa | ||
|
|
7f6db561f5 | ||
|
|
9eac00b303 | ||
|
|
b61df04c1b | ||
|
|
b587e21c90 | ||
|
|
02d66ee9fd | ||
|
|
a950e08ef1 | ||
|
|
6e7d28db7b | ||
|
|
a0ea00d0a3 | ||
|
|
beb5606862 | ||
|
|
44f11f93a4 | ||
|
|
7b61f267dd | ||
|
|
971238cedb | ||
|
|
4d19b9249b | ||
|
|
4046b91609 | ||
|
|
e6652109c5 | ||
|
|
e9cfc066a5 | ||
|
|
9ce078c1d3 | ||
|
|
2bcc1675e8 | ||
|
|
a87c2347c9 | ||
|
|
8f2810db30 | ||
|
|
528d0b0429 | ||
|
|
f759014315 | ||
|
|
a568610c28 | ||
|
|
c35a85db12 | ||
|
|
fcd61436c8 | ||
|
|
0798063213 | ||
|
|
80de63323d | ||
|
|
7d0219b11d | ||
|
|
d42434ddc0 | ||
|
|
cbf6d23c24 | ||
|
|
568c05b4b0 | ||
|
|
59ee4babeb | ||
|
|
604ef4d004 |
9
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Hoppscotch",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
|
||||
"forwardPorts": [3000],
|
||||
"features": {
|
||||
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
|
||||
},
|
||||
"postCreateCommand": "mv .env.example .env && pnpm i"
|
||||
}
|
||||
@@ -14,14 +14,22 @@ VITE_MESSAGING_SENDER_ID=421993993223
|
||||
VITE_APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
|
||||
VITE_MEASUREMENT_ID=G-BBJ3R80PJT
|
||||
|
||||
# Base URL
|
||||
# Base URLs
|
||||
VITE_BASE_URL=https://hoppscotch.io
|
||||
VITE_SHORTCODE_BASE_URL=https://hopp.sh
|
||||
|
||||
# Backend URLs
|
||||
VITE_BACKEND_GQL_URL=https://api.hoppscotch.io/graphql
|
||||
VITE_BACKEND_WS_URL=wss://api.hoppscotch.io/graphql
|
||||
|
||||
# Terms Of Service And Privacy Policy Links (Optional)
|
||||
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/terms
|
||||
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/privacy
|
||||
|
||||
# Sentry (Optional)
|
||||
# VITE_SENTRY_DSN: <Sentry DSN here>
|
||||
# VITE_SENTRY_ENVIRONMENT: <Sentry environment value here>
|
||||
# VITE_SENTRY_RELEASE_TAG: <Sentry release tag here (for release monitoring)>
|
||||
|
||||
# Proxyscotch Access Token (Optional)
|
||||
# VITE_PROXYSCOTCH_ACCESS_TOKEN: <Token Set In Proxyscotch Server>
|
||||
2
.github/dependabot.yml
vendored
@@ -5,6 +5,6 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '00:00'
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 0
|
||||
reviewers:
|
||||
- liyasthomas
|
||||
|
||||
93
.github/workflows/codeql-analysis.yml
vendored
@@ -1,72 +1,63 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: "CodeQL analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '39 7 * * 2'
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: '30 1 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
# Run extended queries including queries using machine learning
|
||||
queries: security-extended
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
# Run extended queries including queries using machine learning
|
||||
queries: security-extended
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below).
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following
|
||||
# three lines and modify them (or add more) to build your code if your
|
||||
# project uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
name: Deploy to Live Channel
|
||||
name: Deploy to Firebase (production)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy_live_website:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to Firebase (production)
|
||||
uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_POSTWOMAN_API }}'
|
||||
@@ -1,27 +1,33 @@
|
||||
name: Deploy to Netlify
|
||||
name: Deploy to Netlify (production)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Push build files to Netlify
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
|
||||
- name: Setup Environment
|
||||
run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Build Site
|
||||
- name: Build site
|
||||
env:
|
||||
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_SENTRY_ENVIRONMENT: production
|
||||
@@ -30,12 +36,12 @@ jobs:
|
||||
|
||||
# Deploy the production site with netlify-cli
|
||||
- name: Deploy to Netlify (production)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-app/dist --prod
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-web/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry Release
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
@@ -1,25 +1,34 @@
|
||||
name: Deploy to Staging Netlify
|
||||
name: Deploy to Netlify (staging)
|
||||
|
||||
on:
|
||||
push:
|
||||
# TODO: Migrate to staging branch only
|
||||
branches: [main]
|
||||
branches: [staging]
|
||||
pull_request:
|
||||
branches: [staging]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Push build files to Netlify
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
env:
|
||||
VITE_BACKEND_GQL_URL: ${{ secrets.STAGING_BACKEND_GQL_URL }}
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
|
||||
- name: Build Site
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Build site
|
||||
env:
|
||||
VITE_GA_ID: ${{ secrets.STAGING_GA_ID }}
|
||||
VITE_GTM_ID: ${{ secrets.STAGING_GTM_ID }}
|
||||
@@ -40,12 +49,12 @@ jobs:
|
||||
|
||||
# Deploy the staging site with netlify-cli
|
||||
- name: Deploy to Netlify (staging)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-app/dist --prod
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-web/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry Release
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
41
.github/workflows/deploy-netlify-ui.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Deploy to Netlify (ui)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# run this workflow only if an update is made to the ui package
|
||||
paths:
|
||||
- "packages/hoppscotch-ui/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Build site
|
||||
run: pnpm run generate-ui
|
||||
|
||||
# Deploy the ui site with netlify-cli
|
||||
- name: Deploy to Netlify (ui)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
17
.github/workflows/publish-docker.yml
vendored
@@ -7,11 +7,20 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
root-reserve-mb: 8192
|
||||
swap-size-mb: 18432
|
||||
remove-dotnet: 'true'
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
@@ -41,6 +50,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
27
.github/workflows/tests.yml
vendored
@@ -2,12 +2,13 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, staging]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [main, staging]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -15,17 +16,23 @@ jobs:
|
||||
node-version: ["lts/*"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
3
.gitignore
vendored
@@ -168,3 +168,6 @@ tests/*/videos
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# PNPM
|
||||
.pnpm-store
|
||||
|
||||
1
.vscode/extensions.json
vendored
@@ -6,6 +6,7 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"csstools.postcss",
|
||||
"folke.vscode-monorepo-workspace"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"octref.vetur"
|
||||
|
||||
27
CODEOWNERS
Normal file
@@ -0,0 +1,27 @@
|
||||
# CODEOWNERS is prioritized from bottom to top
|
||||
|
||||
# If none of the below matched
|
||||
* @AndrewBastin @liyasthomas
|
||||
|
||||
# Packages
|
||||
/packages/codemirror-lang-graphql/ @AndrewBastin
|
||||
/packages/hoppscotch-cli/ @AndrewBastin
|
||||
/packages/hoppscotch-common/ @amk-dev @AndrewBastin
|
||||
/packages/hoppscotch-data/ @AndrewBastin
|
||||
/packages/hoppscotch-js-sandbox/ @AndrewBastin
|
||||
/packages/hoppscotch-ui/ @anwarulislam
|
||||
/packages/hoppscotch-web/ @amk-dev
|
||||
|
||||
# Sections within Hoppscotch Common
|
||||
/packages/hoppscotch-common/src/components @anwarulislam
|
||||
/packages/hoppscotch-common/src/components/collections @nivedin @amk-dev
|
||||
/packages/hoppscotch-common/src/components/environments @nivedin @amk-dev
|
||||
/packages/hoppscotch-common/src/composables @amk-dev
|
||||
/packages/hoppscotch-common/src/modules @AndrewBastin @amk-dev
|
||||
/packages/hoppscotch-common/src/pages @AndrewBastin @amk-dev
|
||||
/packages/hoppscotch-common/src/newstore @AndrewBastin @amk-dev
|
||||
|
||||
README.md @liyasthomas
|
||||
|
||||
# The lockfile has no owner
|
||||
pnpm-lock.yaml
|
||||
@@ -17,13 +17,13 @@ COPY . .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN mv .env.example .env
|
||||
|
||||
RUN pnpm i --unsafe-perm=true
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
EXPOSE 3000
|
||||
|
||||
RUN mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
|
||||
|
||||
RUN pnpm run generate
|
||||
|
||||
CMD ["pnpm", "run", "start"]
|
||||
|
||||
12
README.md
@@ -36,14 +36,14 @@
|
||||
<p>
|
||||
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
|
||||
<img
|
||||
src="./packages/hoppscotch-app/public/images/banner-light.png"
|
||||
src="./packages/hoppscotch-common/public/images/banner-light.png"
|
||||
alt="Hoppscotch"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
|
||||
<img
|
||||
src="./packages/hoppscotch-app/public/images/banner-dark.png"
|
||||
src="./packages/hoppscotch-common/public/images/banner-dark.png"
|
||||
alt="Hoppscotch"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -275,11 +275,11 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Vue](https://vuejs.org)
|
||||
- [Nuxt](https://nuxtjs.org)
|
||||
- [Vite](https://vitejs.dev)
|
||||
|
||||
## **Developing**
|
||||
|
||||
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/.env.example) file found in `packages/hoppscotch-app` with your own keys and rename it to `.env`.
|
||||
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in the root of repository with your own keys and rename it to `.env`.
|
||||
|
||||
_Sample keys only work with the [production build](https://hoppscotch.io)._
|
||||
|
||||
@@ -315,9 +315,9 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
|
||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
||||
2. Install pnpm using npm by running `npm install -g pnpm`.
|
||||
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
4. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/.env.example) file found in `packages/hoppscotch-app` with your own keys and rename it to `.env`.
|
||||
4. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in the root of repository with your own keys and rename it to `.env`.
|
||||
5. Build the release files with `pnpm run generate`.
|
||||
6. Find the built project in `packages/hoppscotch-app/dist`. Host these files on any [static hosting servers](https://www.pluralsight.com/blog/software-development/where-to-host-your-jamstack-site).
|
||||
6. Find the built project in `packages/hoppscotch-web/dist`. Host these files on any [static hosting servers](https://www.pluralsight.com/blog/software-development/where-to-host-your-jamstack-site).
|
||||
|
||||
## **Contributing**
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ if there is no existing translation, you can create a new one by following these
|
||||
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||
2. **Checkout the `i18n` branch for latest translations.**
|
||||
3. **Create a new branch for your translation with base branch `i18n`.**
|
||||
4. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-app/locales) directory.**
|
||||
5. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/locales/en.json) to the target language file.**
|
||||
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
||||
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
||||
6. **Translate the strings in the target language file.**
|
||||
7. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/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.**
|
||||
9. **Send a pull request.**
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
},
|
||||
"hosting": {
|
||||
"predeploy": [
|
||||
"cd packages/hoppscotch-app && mv .env.example .env && cd ../.. && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||
"mv .env.example .env && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||
],
|
||||
"public": "packages/hoppscotch-app/dist",
|
||||
"public": "packages/hoppscotch-web/dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
[build]
|
||||
base = "/"
|
||||
publish = "packages/hoppscotch-app/dist"
|
||||
publish = "packages/hoppscotch-web/dist"
|
||||
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-Frame-Options = "SAMEORIGIN"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
|
||||
[[redirects]]
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
"prepare": "husky install",
|
||||
"dev": "pnpm -r do-dev",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
"start": "http-server packages/hoppscotch-app/dist -p 3000",
|
||||
"start": "http-server packages/hoppscotch-web/dist -p 3000",
|
||||
"lint": "pnpm -r do-lint",
|
||||
"typecheck": "pnpm -r do-typecheck",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
"pre-commit": "pnpm -r do-lint && pnpm -r do-typecheck",
|
||||
"test": "pnpm -r do-test"
|
||||
"test": "pnpm -r do-test",
|
||||
"generate-ui": "pnpm -r do-build-ui"
|
||||
},
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
@@ -1,13 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 00-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0020 4.77 5.07 5.07 0 0019.91 1S18.73.65 16 2.48a13.38 13.38 0 00-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 005 4.77a5.44 5.44 0 00-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 009 18.13V22" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 504 B |
@@ -1,672 +0,0 @@
|
||||
{
|
||||
"action": {
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Cancel",
|
||||
"choose_file": "Choose a file",
|
||||
"clear": "Clear",
|
||||
"clear_all": "Clear all",
|
||||
"close": "Close",
|
||||
"connect": "Connect",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"disconnect": "Disconnect",
|
||||
"dismiss": "Dismiss",
|
||||
"dont_save": "Don't save",
|
||||
"download_file": "Download file",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
"filter_response": "Filter response",
|
||||
"go_back": "Go back",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"less": "Less",
|
||||
"more": "More",
|
||||
"new": "New",
|
||||
"no": "No",
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Remove",
|
||||
"restore": "Restore",
|
||||
"save": "Save",
|
||||
"scroll_to_bottom": "Scroll to bottom",
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"to_close": "to close",
|
||||
"to_navigate": "to navigate",
|
||||
"to_select": "to select",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on": "Turn on",
|
||||
"undo": "Undo",
|
||||
"yes": "Yes"
|
||||
},
|
||||
"add": {
|
||||
"new": "Add new",
|
||||
"star": "Add star"
|
||||
},
|
||||
"app": {
|
||||
"chat_with_us": "Chat with us",
|
||||
"contact_us": "Contact us",
|
||||
"copy": "Copy",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentation",
|
||||
"github": "GitHub",
|
||||
"help": "Help & feedback",
|
||||
"home": "Home",
|
||||
"invite": "Invite",
|
||||
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
||||
"invite_your_friends": "Invite your friends",
|
||||
"join_discord_community": "Join our Discord community",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "New version found. Refresh to update.",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "Proxy privacy policy",
|
||||
"reload": "Reload",
|
||||
"search": "Search",
|
||||
"share": "Share",
|
||||
"shortcuts": "Shortcuts",
|
||||
"spotlight": "Spotlight",
|
||||
"status": "Status",
|
||||
"status_description": "Check the status of the website",
|
||||
"terms_and_privacy": "Terms and privacy",
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "Type a command or search…",
|
||||
"we_use_cookies": "We use cookies",
|
||||
"whats_new": "What's new?",
|
||||
"wiki": "Wiki"
|
||||
},
|
||||
"auth": {
|
||||
"account_exists": "Account exists with different credential - Login to link both accounts",
|
||||
"all_sign_in_options": "All sign in options",
|
||||
"continue_with_email": "Continue with Email",
|
||||
"continue_with_github": "Continue with GitHub",
|
||||
"continue_with_google": "Continue with Google",
|
||||
"continue_with_microsoft": "Continue with Microsoft",
|
||||
"email": "Email",
|
||||
"logged_out": "Logged out",
|
||||
"login": "Login",
|
||||
"login_success": "Successfully logged in",
|
||||
"login_to_hoppscotch": "Login to Hoppscotch",
|
||||
"logout": "Logout",
|
||||
"re_enter_email": "Re-enter email",
|
||||
"send_magic_link": "Send a magic link",
|
||||
"sync": "Sync",
|
||||
"we_sent_magic_link": "We sent you a magic link!",
|
||||
"we_sent_magic_link_description": "Check your inbox - we sent an email to {email}. It contains a magic link that will log you in."
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generate Token",
|
||||
"include_in_url": "Include in URL",
|
||||
"learn": "Learn how",
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Password",
|
||||
"token": "Token",
|
||||
"type": "Authorization Type",
|
||||
"username": "Username"
|
||||
},
|
||||
"collection": {
|
||||
"created": "Collection created",
|
||||
"edit": "Edit Collection",
|
||||
"invalid_name": "Please provide a name for the collection",
|
||||
"my_collections": "My Collections",
|
||||
"name": "My New Collection",
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "New Collection",
|
||||
"renamed": "Collection renamed",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Save as",
|
||||
"select": "Select a Collection",
|
||||
"select_location": "Select location",
|
||||
"select_team": "Select a team",
|
||||
"team_collections": "Team Collections"
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "Are you sure you want to leave this team?",
|
||||
"logout": "Are you sure you want to logout?",
|
||||
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
||||
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
||||
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||
"remove_team": "Are you sure you want to delete this team?",
|
||||
"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.",
|
||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||
},
|
||||
"count": {
|
||||
"header": "Header {count}",
|
||||
"message": "Message {count}",
|
||||
"parameter": "Parameter {count}",
|
||||
"protocol": "Protocol {count}",
|
||||
"value": "Value {count}",
|
||||
"variable": "Variable {count}"
|
||||
},
|
||||
"documentation": {
|
||||
"generate": "Generate documentation",
|
||||
"generate_message": "Import any Hoppscotch collection to generate API documentation on-the-go."
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "This request does not use any authorization",
|
||||
"body": "This request does not have a body",
|
||||
"collection": "Collection is empty",
|
||||
"collections": "Collections are empty",
|
||||
"documentation": "Connect to a GraphQL endpoint to view documentation",
|
||||
"endpoint": "Endpoint cannot be empty",
|
||||
"environments": "Environments are empty",
|
||||
"folder": "Folder is empty",
|
||||
"headers": "This request does not have any headers",
|
||||
"history": "History is empty",
|
||||
"invites": "Invite list is empty",
|
||||
"members": "Team is empty",
|
||||
"parameters": "This request does not have any parameters",
|
||||
"pending_invites": "There are no pending invites for this team",
|
||||
"profile": "Login in to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"team_name": "Team name empty",
|
||||
"teams": "You don't belong to any teams",
|
||||
"tests": "There are no tests for this request"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Add to Global",
|
||||
"added": "Environment addition",
|
||||
"create_new": "Create new environment",
|
||||
"created": "Environment created",
|
||||
"deleted": "Environment deletion",
|
||||
"edit": "Edit Environment",
|
||||
"invalid_name": "Please provide a name for the environment",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "New Environment",
|
||||
"no_environment": "No environment",
|
||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||
"select": "Select environment",
|
||||
"title": "Environments",
|
||||
"updated": "Environment updated",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||
"check_console_details": "Check console log for details.",
|
||||
"curl_invalid_format": "cURL is not formatted properly",
|
||||
"empty_req_name": "Empty Request Name",
|
||||
"f12_details": "(F12 for details)",
|
||||
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
||||
"incomplete_config_urls": "Incomplete configuration URLs",
|
||||
"incorrect_email": "Incorrect email",
|
||||
"invalid_link": "Invalid link",
|
||||
"invalid_link_description": "The link you clicked is invalid or expired.",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Could not send request",
|
||||
"no_duration": "No duration",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
},
|
||||
"export": {
|
||||
"as_json": "Export as JSON",
|
||||
"create_secret_gist": "Create secret Gist",
|
||||
"gist_created": "Gist created",
|
||||
"require_github": "Login with GitHub to create secret gist",
|
||||
"title": "Export"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Folder created",
|
||||
"edit": "Edit Folder",
|
||||
"invalid_name": "Please provide a name for the folder",
|
||||
"name_length_insufficient": "Folder name should be at least 3 characters long",
|
||||
"new": "New Folder",
|
||||
"renamed": "Folder renamed"
|
||||
},
|
||||
"graphql": {
|
||||
"mutations": "Mutations",
|
||||
"schema": "Schema",
|
||||
"subscriptions": "Subscriptions"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Install app",
|
||||
"login": "Login",
|
||||
"save_workspace": "Save My Workspace"
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||
"generate_documentation_first": "Generate documentation first",
|
||||
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
||||
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
|
||||
"offline_short": "You seem to be offline.",
|
||||
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
||||
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
||||
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
||||
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
||||
"tests": "Write a test script to automate debugging."
|
||||
},
|
||||
"hide": {
|
||||
"collection": "Collapse Collection Panel",
|
||||
"more": "Hide more",
|
||||
"preview": "Hide Preview",
|
||||
"sidebar": "Collapse sidebar"
|
||||
},
|
||||
"import": {
|
||||
"collections": "Import collections",
|
||||
"curl": "Import cURL",
|
||||
"failed": "Error while importing: format not recognized",
|
||||
"from_gist": "Import from Gist",
|
||||
"from_gist_description": "Import from Gist URL",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_insomnia_description": "Import from Insomnia collection",
|
||||
"from_json": "Import from Hoppscotch",
|
||||
"from_json_description": "Import from Hoppscotch collection file",
|
||||
"from_my_collections": "Import from My Collections",
|
||||
"from_my_collections_description": "Import from My Collections file",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Enter Gist URL",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "Collapse or Expand Collections",
|
||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
||||
"column": "Vertical layout",
|
||||
"name": "Layout",
|
||||
"row": "Horizontal layout",
|
||||
"zen_mode": "Zen mode"
|
||||
},
|
||||
"modal": {
|
||||
"collections": "Collections",
|
||||
"confirm": "Confirm",
|
||||
"edit_request": "Edit Request",
|
||||
"import_export": "Import / Export"
|
||||
},
|
||||
"mqtt": {
|
||||
"communication": "Communication",
|
||||
"log": "Log",
|
||||
"message": "Message",
|
||||
"publish": "Publish",
|
||||
"subscribe": "Subscribe",
|
||||
"topic": "Topic",
|
||||
"topic_name": "Topic Name",
|
||||
"topic_title": "Publish / Subscribe topic",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"url": "URL"
|
||||
},
|
||||
"navigation": {
|
||||
"doc": "Docs",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "Profile",
|
||||
"realtime": "Realtime",
|
||||
"rest": "REST",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"preRequest": {
|
||||
"javascript_code": "JavaScript Code",
|
||||
"learn": "Read documentation",
|
||||
"script": "Pre-Request Script",
|
||||
"snippets": "Snippets"
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "App Settings",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
"no_permission": "You do not have permission to perform this action.",
|
||||
"owner": "Owner",
|
||||
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
|
||||
"roles": "Roles",
|
||||
"roles_description": "Roles are used to control access to the shared collections.",
|
||||
"updated": "Profile updated",
|
||||
"viewer": "Viewer",
|
||||
"viewer_description": "Viewers can only view and use requests."
|
||||
},
|
||||
"remove": {
|
||||
"star": "Remove star"
|
||||
},
|
||||
"request": {
|
||||
"added": "Request added",
|
||||
"authorization": "Authorization",
|
||||
"body": "Request Body",
|
||||
"choose_language": "Choose language",
|
||||
"content_type": "Content Type",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Copy link",
|
||||
"duration": "Duration",
|
||||
"enter_curl": "Enter cURL",
|
||||
"generate_code": "Generate code",
|
||||
"generated_code": "Generated code",
|
||||
"header_list": "Header List",
|
||||
"invalid_name": "Please provide a name for the request",
|
||||
"method": "Method",
|
||||
"name": "Request name",
|
||||
"new": "New Request",
|
||||
"override": "Override",
|
||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
||||
"overriden": "Overridden",
|
||||
"parameter_list": "Query Parameters",
|
||||
"parameters": "Parameters",
|
||||
"path": "Path",
|
||||
"payload": "Payload",
|
||||
"query": "Query",
|
||||
"raw_body": "Raw Request Body",
|
||||
"renamed": "Request renamed",
|
||||
"run": "Run",
|
||||
"save": "Save",
|
||||
"save_as": "Save as",
|
||||
"saved": "Request saved",
|
||||
"share": "Share",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"title": "Request",
|
||||
"type": "Request type",
|
||||
"url": "URL",
|
||||
"variables": "Variables",
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
"html": "HTML",
|
||||
"image": "Image",
|
||||
"json": "JSON",
|
||||
"pdf": "PDF",
|
||||
"preview_html": "Preview HTML",
|
||||
"raw": "Raw",
|
||||
"size": "Size",
|
||||
"status": "Status",
|
||||
"time": "Time",
|
||||
"title": "Response",
|
||||
"waiting_for_connection": "waiting for connection",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Accent color",
|
||||
"account": "アカウント",
|
||||
"account_description": "Customize your account settings.",
|
||||
"account_email_description": "Your primary email address.",
|
||||
"account_name_description": "This is your display name.",
|
||||
"background": "Background",
|
||||
"black_mode": "Black",
|
||||
"change_font_size": "Change font size",
|
||||
"choose_language": "Choose language",
|
||||
"dark_mode": "Dark",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Experiments",
|
||||
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
|
||||
"extension_ver_not_reported": "Not Reported",
|
||||
"extension_version": "Extension Version",
|
||||
"extensions": "Browser extension",
|
||||
"extensions_use_toggle": "Use the browser extension to send requests (if present)",
|
||||
"follow": "Follow Us",
|
||||
"font_size": "Font size",
|
||||
"font_size_large": "Large",
|
||||
"font_size_medium": "Medium",
|
||||
"font_size_small": "Small",
|
||||
"interceptor": "Interceptor",
|
||||
"interceptor_description": "Middleware between application and APIs.",
|
||||
"language": "Language",
|
||||
"light_mode": "Light",
|
||||
"official_proxy_hosting": "Official Proxy is hosted by Hoppscotch.",
|
||||
"profile": "Profile",
|
||||
"profile_description": "Update your profile details",
|
||||
"profile_email": "Email address",
|
||||
"profile_name": "Profile name",
|
||||
"proxy": "Proxy",
|
||||
"proxy_url": "Proxy URL",
|
||||
"proxy_use_toggle": "Use the proxy middleware to send requests",
|
||||
"read_the": "Read the",
|
||||
"reset_default": "Reset to default",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"sidebar_on_left": "Sidebar on left",
|
||||
"sync": "Synchronise",
|
||||
"sync_collections": "Collections",
|
||||
"sync_description": "These settings are synced to cloud.",
|
||||
"sync_environments": "Environments",
|
||||
"sync_history": "History",
|
||||
"system_mode": "System",
|
||||
"telemetry": "Telemetry",
|
||||
"telemetry_helps_us": "Telemetry helps us to personalize our operations and deliver the best experience to you.",
|
||||
"theme": "Theme",
|
||||
"theme_description": "Customize your application theme.",
|
||||
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
|
||||
"user": "User",
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
"close_current_menu": "Close current menu",
|
||||
"command_menu": "Search & command menu",
|
||||
"help_menu": "Help menu",
|
||||
"show_all": "Keyboard shortcuts",
|
||||
"title": "General"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite people to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
},
|
||||
"navigation": {
|
||||
"back": "Go back to previous page",
|
||||
"documentation": "Go to Documentation page",
|
||||
"forward": "Go forward to next page",
|
||||
"graphql": "Go to GraphQL page",
|
||||
"profile": "Go to Profile page",
|
||||
"realtime": "Go to Realtime page",
|
||||
"rest": "Go to REST page",
|
||||
"settings": "Go to Settings page",
|
||||
"title": "Navigation"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copy Request Link",
|
||||
"delete_method": "Select DELETE method",
|
||||
"get_method": "Select GET method",
|
||||
"head_method": "Select HEAD method",
|
||||
"method": "Method",
|
||||
"next_method": "Select Next method",
|
||||
"post_method": "Select POST method",
|
||||
"previous_method": "Select Previous method",
|
||||
"put_method": "Select PUT method",
|
||||
"reset_request": "Reset Request",
|
||||
"save_to_collections": "Save to Collections",
|
||||
"send_request": "Send Request",
|
||||
"title": "Request"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Switch theme to black mode",
|
||||
"dark": "Switch theme to dark mode",
|
||||
"light": "Switch theme to light mode",
|
||||
"system": "Switch theme to system mode",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"code": "Show code",
|
||||
"collection": "Expand Collection Panel",
|
||||
"more": "Show more",
|
||||
"sidebar": "Expand sidebar"
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Communication",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"event_name": "Event Name",
|
||||
"events": "Events",
|
||||
"log": "Log",
|
||||
"url": "URL"
|
||||
},
|
||||
"sse": {
|
||||
"event_type": "Event type",
|
||||
"log": "Log",
|
||||
"url": "URL"
|
||||
},
|
||||
"state": {
|
||||
"bulk_mode": "Bulk edit",
|
||||
"bulk_mode_placeholder": "Entries are separated by newline\nKeys and values are separated by :\nPrepend # to any row you want to add but keep disabled",
|
||||
"cleared": "Cleared",
|
||||
"connected": "Connected",
|
||||
"connected_to": "Connected to {name}",
|
||||
"connecting_to": "Connecting to {name}...",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"copied_to_clipboard": "Copied to clipboard",
|
||||
"deleted": "Deleted",
|
||||
"deprecated": "DEPRECATED",
|
||||
"disabled": "Disabled",
|
||||
"disconnected": "Disconnected",
|
||||
"disconnected_from": "Disconnected from {name}",
|
||||
"docs_generated": "Documentation generated",
|
||||
"download_started": "Download started",
|
||||
"enabled": "Enabled",
|
||||
"file_imported": "File imported",
|
||||
"finished_in": "Finished in {duration} ms",
|
||||
"history_deleted": "History deleted",
|
||||
"linewrap": "Wrap lines",
|
||||
"loading": "Loading...",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"none": "None",
|
||||
"nothing_found": "Nothing found for",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"waiting_send_request": "Waiting to send request"
|
||||
},
|
||||
"support": {
|
||||
"changelog": "Read more about latest releases",
|
||||
"chat": "Questions? Chat with us!",
|
||||
"community": "Ask questions and help others",
|
||||
"documentation": "Read more about Hoppscotch",
|
||||
"forum": "Ask questions and get answers",
|
||||
"github": "Follow us on Github",
|
||||
"shortcuts": "Browse app faster",
|
||||
"team": "Get in touch with the team",
|
||||
"title": "Support",
|
||||
"twitter": "Follow us on Twitter"
|
||||
},
|
||||
"tab": {
|
||||
"authorization": "Authorization",
|
||||
"body": "Body",
|
||||
"collections": "Collections",
|
||||
"documentation": "Documentation",
|
||||
"headers": "Headers",
|
||||
"history": "History",
|
||||
"mqtt": "MQTT",
|
||||
"parameters": "Parameters",
|
||||
"pre_request_script": "Pre-request Script",
|
||||
"queries": "Queries",
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Tests",
|
||||
"types": "Types",
|
||||
"variables": "Variables",
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"team": {
|
||||
"already_member": "You are already a member of this team. Contact your team owner.",
|
||||
"create_new": "Create new team",
|
||||
"deleted": "Team deleted",
|
||||
"edit": "Edit Team",
|
||||
"email": "E-mail",
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
||||
"exit": "Exit Team",
|
||||
"exit_disabled": "Only owner cannot exit the team",
|
||||
"invalid_email_format": "Email format is invalid",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
"invalid_invite_link": "Invalid invite link",
|
||||
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
|
||||
"invalid_member_permission": "Please provide a valid permission to the team member",
|
||||
"invite": "Invite",
|
||||
"invite_more": "Invite more",
|
||||
"invite_tooltip": "Invite people to this workspace",
|
||||
"invited_to_team": "{owner} invited you to join {team}",
|
||||
"join": "Invitation accepted",
|
||||
"join_beta": "Join the beta program to access teams.",
|
||||
"join_team": "Join {team}",
|
||||
"joined_team": "You have joined {team}",
|
||||
"joined_team_description": "You are now a member of this team",
|
||||
"left": "You left the team",
|
||||
"login_to_continue": "Login to continue",
|
||||
"login_to_continue_description": "You need to be logged in to join a team.",
|
||||
"logout_and_try_again": "Logout and sign in with another account",
|
||||
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
|
||||
"member_not_found": "Member not found. Contact your team owner.",
|
||||
"member_removed": "User removed",
|
||||
"member_role_updated": "User roles updated",
|
||||
"members": "Members",
|
||||
"name_length_insufficient": "Team name should be at least 6 characters long",
|
||||
"name_updated": "Team name updated",
|
||||
"new": "New Team",
|
||||
"new_created": "New team created",
|
||||
"new_name": "My New Team",
|
||||
"no_access": "You do not have edit access to these collections",
|
||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
|
||||
"pending_invites": "Pending invites",
|
||||
"permissions": "Permissions",
|
||||
"saved": "Team saved",
|
||||
"select_a_team": "Select a team",
|
||||
"title": "Teams",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
},
|
||||
"test": {
|
||||
"failed": "test failed",
|
||||
"javascript_code": "JavaScript Code",
|
||||
"learn": "Read documentation",
|
||||
"passed": "test passed",
|
||||
"report": "Test Report",
|
||||
"results": "Test Results",
|
||||
"script": "Script",
|
||||
"snippets": "Snippets"
|
||||
},
|
||||
"websocket": {
|
||||
"communication": "Communication",
|
||||
"log": "Log",
|
||||
"message": "Message",
|
||||
"protocols": "Protocols",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
272
packages/hoppscotch-app/shims-volar.d.ts
vendored
@@ -1,272 +0,0 @@
|
||||
import AppAnnouncement from "./components/app/Announcement.vue";
|
||||
import AppDeveloperOptions from "./components/app/DeveloperOptions.vue";
|
||||
import AppFooter from "./components/app/Footer.vue";
|
||||
import AppFuse from "./components/app/Fuse.vue";
|
||||
import AppGitHubStarButton from "./components/app/GitHubStarButton.vue";
|
||||
import AppHeader from "./components/app/Header.vue";
|
||||
import AppInterceptor from "./components/app/Interceptor.vue";
|
||||
import AppLogo from "./components/app/Logo.vue";
|
||||
import AppOptions from "./components/app/Options.vue";
|
||||
import AppPaneLayout from "./components/app/PaneLayout.vue";
|
||||
import AppPowerSearch from "./components/app/PowerSearch.vue";
|
||||
import AppPowerSearchEntry from "./components/app/PowerSearchEntry.vue";
|
||||
import AppShare from "./components/app/Share.vue";
|
||||
import AppShortcuts from "./components/app/Shortcuts.vue";
|
||||
import AppShortcutsEntry from "./components/app/ShortcutsEntry.vue";
|
||||
import AppSidenav from "./components/app/Sidenav.vue";
|
||||
import AppSlideOver from "./components/app/SlideOver.vue";
|
||||
import AppSupport from "./components/app/Support.vue";
|
||||
import ButtonPrimary from "./components/button/Primary.vue";
|
||||
import ButtonSecondary from "./components/button/Secondary.vue";
|
||||
import CollectionsAdd from "./components/collections/Add.vue";
|
||||
import CollectionsAddFolder from "./components/collections/AddFolder.vue";
|
||||
import CollectionsAddRequest from "./components/collections/AddRequest.vue";
|
||||
import CollectionsChooseType from "./components/collections/ChooseType.vue";
|
||||
import CollectionsEdit from "./components/collections/Edit.vue";
|
||||
import CollectionsEditFolder from "./components/collections/EditFolder.vue";
|
||||
import CollectionsEditRequest from "./components/collections/EditRequest.vue";
|
||||
import CollectionsImportExport from "./components/collections/ImportExport.vue";
|
||||
import CollectionsSaveRequest from "./components/collections/SaveRequest.vue";
|
||||
import CollectionsGraphqlAdd from "./components/collections/graphql/Add.vue";
|
||||
import CollectionsGraphqlAddFolder from "./components/collections/graphql/AddFolder.vue";
|
||||
import CollectionsGraphqlAddRequest from "./components/collections/graphql/AddRequest.vue";
|
||||
import CollectionsGraphqlCollection from "./components/collections/graphql/Collection.vue";
|
||||
import CollectionsGraphqlEdit from "./components/collections/graphql/Edit.vue";
|
||||
import CollectionsGraphqlEditFolder from "./components/collections/graphql/EditFolder.vue";
|
||||
import CollectionsGraphqlEditRequest from "./components/collections/graphql/EditRequest.vue";
|
||||
import CollectionsGraphqlFolder from "./components/collections/graphql/Folder.vue";
|
||||
import CollectionsGraphqlImportExport from "./components/collections/graphql/ImportExport.vue";
|
||||
import CollectionsGraphqlRequest from "./components/collections/graphql/Request.vue";
|
||||
import CollectionsGraphql from "./components/collections/graphql/index.vue";
|
||||
import Collections from "./components/collections/index.vue";
|
||||
import CollectionsMyCollection from "./components/collections/my/Collection.vue";
|
||||
import CollectionsMyFolder from "./components/collections/my/Folder.vue";
|
||||
import CollectionsMyRequest from "./components/collections/my/Request.vue";
|
||||
import CollectionsTeamsCollection from "./components/collections/teams/Collection.vue";
|
||||
import CollectionsTeamsFolder from "./components/collections/teams/Folder.vue";
|
||||
import CollectionsTeamsRequest from "./components/collections/teams/Request.vue";
|
||||
import DocsCollection from "./components/docs/Collection.vue";
|
||||
import DocsFolder from "./components/docs/Folder.vue";
|
||||
import DocsRequest from "./components/docs/Request.vue";
|
||||
import EnvironmentsDetails from "./components/environments/Details.vue";
|
||||
import EnvironmentsEnvironment from "./components/environments/Environment.vue";
|
||||
import EnvironmentsImportExport from "./components/environments/ImportExport.vue";
|
||||
import Environments from "./components/environments/index.vue";
|
||||
import FirebaseLogin from "./components/firebase/Login.vue";
|
||||
import FirebaseLogout from "./components/firebase/Logout.vue";
|
||||
import GraphqlAuthorization from "./components/graphql/Authorization.vue";
|
||||
import GraphqlField from "./components/graphql/Field.vue";
|
||||
import GraphqlRequest from "./components/graphql/Request.vue";
|
||||
import GraphqlRequestOptions from "./components/graphql/RequestOptions.vue";
|
||||
import GraphqlResponse from "./components/graphql/Response.vue";
|
||||
import GraphqlSidebar from "./components/graphql/Sidebar.vue";
|
||||
import GraphqlType from "./components/graphql/Type.vue";
|
||||
import GraphqlTypeLink from "./components/graphql/TypeLink.vue";
|
||||
import HistoryGraphqlCard from "./components/history/graphql/Card.vue";
|
||||
import History from "./components/history/index.vue";
|
||||
import HistoryRestCard from "./components/history/rest/Card.vue";
|
||||
import HttpAuthorization from "./components/http/Authorization.vue";
|
||||
import HttpBody from "./components/http/Body.vue";
|
||||
import HttpBodyParameters from "./components/http/BodyParameters.vue";
|
||||
import HttpCodegenModal from "./components/http/CodegenModal.vue";
|
||||
import HttpHeaders from "./components/http/Headers.vue";
|
||||
import HttpImportCurl from "./components/http/ImportCurl.vue";
|
||||
import HttpOAuth2Authorization from "./components/http/OAuth2Authorization.vue";
|
||||
import HttpParameters from "./components/http/Parameters.vue";
|
||||
import HttpPreRequestScript from "./components/http/PreRequestScript.vue";
|
||||
import HttpRawBody from "./components/http/RawBody.vue";
|
||||
import HttpReqChangeConfirmModal from "./components/http/ReqChangeConfirmModal.vue";
|
||||
import HttpRequest from "./components/http/Request.vue";
|
||||
import HttpRequestOptions from "./components/http/RequestOptions.vue";
|
||||
import HttpResponse from "./components/http/Response.vue";
|
||||
import HttpResponseMeta from "./components/http/ResponseMeta.vue";
|
||||
import HttpSidebar from "./components/http/Sidebar.vue";
|
||||
import HttpTestResult from "./components/http/TestResult.vue";
|
||||
import HttpTestResultEntry from "./components/http/TestResultEntry.vue";
|
||||
import HttpTestResultEnv from "./components/http/TestResultEnv.vue";
|
||||
import HttpTestResultReport from "./components/http/TestResultReport.vue";
|
||||
import HttpTests from "./components/http/Tests.vue";
|
||||
import HttpURLEncodedParams from "./components/http/URLEncodedParams.vue";
|
||||
import LensesHeadersRenderer from "./components/lenses/HeadersRenderer.vue";
|
||||
import LensesHeadersRendererEntry from "./components/lenses/HeadersRendererEntry.vue";
|
||||
import LensesResponseBodyRenderer from "./components/lenses/ResponseBodyRenderer.vue";
|
||||
import LensesRenderersHTMLLensRenderer from "./components/lenses/renderers/HTMLLensRenderer.vue";
|
||||
import LensesRenderersImageLensRenderer from "./components/lenses/renderers/ImageLensRenderer.vue";
|
||||
import LensesRenderersJSONLensRenderer from "./components/lenses/renderers/JSONLensRenderer.vue";
|
||||
import LensesRenderersPDFLensRenderer from "./components/lenses/renderers/PDFLensRenderer.vue";
|
||||
import LensesRenderersRawLensRenderer from "./components/lenses/renderers/RawLensRenderer.vue";
|
||||
import LensesRenderersXMLLensRenderer from "./components/lenses/renderers/XMLLensRenderer.vue";
|
||||
import ProfilePicture from "./components/profile/Picture.vue";
|
||||
import ProfileShortcode from "./components/profile/Shortcode.vue";
|
||||
import RealtimeCommunication from "./components/realtime/Communication.vue";
|
||||
import RealtimeLog from "./components/realtime/Log.vue";
|
||||
import RealtimeLogEntry from "./components/realtime/LogEntry.vue";
|
||||
import SmartAccentModePicker from "./components/smart/AccentModePicker.vue";
|
||||
import SmartAnchor from "./components/smart/Anchor.vue";
|
||||
import SmartAutoComplete from "./components/smart/AutoComplete.vue";
|
||||
import SmartChangeLanguage from "./components/smart/ChangeLanguage.vue";
|
||||
import SmartCheckbox from "./components/smart/Checkbox.vue";
|
||||
import SmartColorModePicker from "./components/smart/ColorModePicker.vue";
|
||||
import SmartConfirmModal from "./components/smart/ConfirmModal.vue";
|
||||
import SmartEnvInput from "./components/smart/EnvInput.vue";
|
||||
import SmartExpand from "./components/smart/Expand.vue";
|
||||
import SmartFileChip from "./components/smart/FileChip.vue";
|
||||
import SmartFontSizePicker from "./components/smart/FontSizePicker.vue";
|
||||
import SmartIcon from "./components/smart/Icon.vue";
|
||||
import SmartIntersection from "./components/smart/Intersection.vue";
|
||||
import SmartItem from "./components/smart/Item.vue";
|
||||
import SmartLoadingIndicator from "./components/smart/LoadingIndicator.vue";
|
||||
import SmartModal from "./components/smart/Modal.vue";
|
||||
import SmartProgressRing from "./components/smart/ProgressRing.vue";
|
||||
import SmartRadio from "./components/smart/Radio.vue";
|
||||
import SmartRadioGroup from "./components/smart/RadioGroup.vue";
|
||||
import SmartSpinner from "./components/smart/Spinner.vue";
|
||||
import SmartTab from "./components/smart/Tab.vue";
|
||||
import SmartTabs from "./components/smart/Tabs.vue";
|
||||
import SmartToggle from "./components/smart/Toggle.vue";
|
||||
import TabPrimary from "./components/tab/Primary.vue";
|
||||
import TabSecondary from "./components/tab/Secondary.vue";
|
||||
import TeamsAdd from "./components/teams/Add.vue";
|
||||
import TeamsEdit from "./components/teams/Edit.vue";
|
||||
import TeamsInvite from "./components/teams/Invite.vue";
|
||||
import TeamsModal from "./components/teams/Modal.vue";
|
||||
import TeamsTeam from "./components/teams/Team.vue";
|
||||
import Teams from "./components/teams/index.vue";
|
||||
declare global {
|
||||
interface __VLS_GlobalComponents {
|
||||
AppAnnouncement: typeof AppAnnouncement;
|
||||
AppDeveloperOptions: typeof AppDeveloperOptions;
|
||||
AppFooter: typeof AppFooter;
|
||||
AppFuse: typeof AppFuse;
|
||||
AppGitHubStarButton: typeof AppGitHubStarButton;
|
||||
AppHeader: typeof AppHeader;
|
||||
AppInterceptor: typeof AppInterceptor;
|
||||
AppLogo: typeof AppLogo;
|
||||
AppOptions: typeof AppOptions;
|
||||
AppPaneLayout: typeof AppPaneLayout;
|
||||
AppPowerSearch: typeof AppPowerSearch;
|
||||
AppPowerSearchEntry: typeof AppPowerSearchEntry;
|
||||
AppShare: typeof AppShare;
|
||||
AppShortcuts: typeof AppShortcuts;
|
||||
AppShortcutsEntry: typeof AppShortcutsEntry;
|
||||
AppSidenav: typeof AppSidenav;
|
||||
AppSlideOver: typeof AppSlideOver;
|
||||
AppSupport: typeof AppSupport;
|
||||
ButtonPrimary: typeof ButtonPrimary;
|
||||
ButtonSecondary: typeof ButtonSecondary;
|
||||
CollectionsAdd: typeof CollectionsAdd;
|
||||
CollectionsAddFolder: typeof CollectionsAddFolder;
|
||||
CollectionsAddRequest: typeof CollectionsAddRequest;
|
||||
CollectionsChooseType: typeof CollectionsChooseType;
|
||||
CollectionsEdit: typeof CollectionsEdit;
|
||||
CollectionsEditFolder: typeof CollectionsEditFolder;
|
||||
CollectionsEditRequest: typeof CollectionsEditRequest;
|
||||
CollectionsImportExport: typeof CollectionsImportExport;
|
||||
CollectionsSaveRequest: typeof CollectionsSaveRequest;
|
||||
CollectionsGraphqlAdd: typeof CollectionsGraphqlAdd;
|
||||
CollectionsGraphqlAddFolder: typeof CollectionsGraphqlAddFolder;
|
||||
CollectionsGraphqlAddRequest: typeof CollectionsGraphqlAddRequest;
|
||||
CollectionsGraphqlCollection: typeof CollectionsGraphqlCollection;
|
||||
CollectionsGraphqlEdit: typeof CollectionsGraphqlEdit;
|
||||
CollectionsGraphqlEditFolder: typeof CollectionsGraphqlEditFolder;
|
||||
CollectionsGraphqlEditRequest: typeof CollectionsGraphqlEditRequest;
|
||||
CollectionsGraphqlFolder: typeof CollectionsGraphqlFolder;
|
||||
CollectionsGraphqlImportExport: typeof CollectionsGraphqlImportExport;
|
||||
CollectionsGraphqlRequest: typeof CollectionsGraphqlRequest;
|
||||
CollectionsGraphql: typeof CollectionsGraphql;
|
||||
Collections: typeof Collections;
|
||||
CollectionsMyCollection: typeof CollectionsMyCollection;
|
||||
CollectionsMyFolder: typeof CollectionsMyFolder;
|
||||
CollectionsMyRequest: typeof CollectionsMyRequest;
|
||||
CollectionsTeamsCollection: typeof CollectionsTeamsCollection;
|
||||
CollectionsTeamsFolder: typeof CollectionsTeamsFolder;
|
||||
CollectionsTeamsRequest: typeof CollectionsTeamsRequest;
|
||||
DocsCollection: typeof DocsCollection;
|
||||
DocsFolder: typeof DocsFolder;
|
||||
DocsRequest: typeof DocsRequest;
|
||||
EnvironmentsDetails: typeof EnvironmentsDetails;
|
||||
EnvironmentsEnvironment: typeof EnvironmentsEnvironment;
|
||||
EnvironmentsImportExport: typeof EnvironmentsImportExport;
|
||||
Environments: typeof Environments;
|
||||
FirebaseLogin: typeof FirebaseLogin;
|
||||
FirebaseLogout: typeof FirebaseLogout;
|
||||
GraphqlAuthorization: typeof GraphqlAuthorization;
|
||||
GraphqlField: typeof GraphqlField;
|
||||
GraphqlRequest: typeof GraphqlRequest;
|
||||
GraphqlRequestOptions: typeof GraphqlRequestOptions;
|
||||
GraphqlResponse: typeof GraphqlResponse;
|
||||
GraphqlSidebar: typeof GraphqlSidebar;
|
||||
GraphqlType: typeof GraphqlType;
|
||||
GraphqlTypeLink: typeof GraphqlTypeLink;
|
||||
HistoryGraphqlCard: typeof HistoryGraphqlCard;
|
||||
History: typeof History;
|
||||
HistoryRestCard: typeof HistoryRestCard;
|
||||
HttpAuthorization: typeof HttpAuthorization;
|
||||
HttpBody: typeof HttpBody;
|
||||
HttpBodyParameters: typeof HttpBodyParameters;
|
||||
HttpCodegenModal: typeof HttpCodegenModal;
|
||||
HttpHeaders: typeof HttpHeaders;
|
||||
HttpImportCurl: typeof HttpImportCurl;
|
||||
HttpOAuth2Authorization: typeof HttpOAuth2Authorization;
|
||||
HttpParameters: typeof HttpParameters;
|
||||
HttpPreRequestScript: typeof HttpPreRequestScript;
|
||||
HttpRawBody: typeof HttpRawBody;
|
||||
HttpReqChangeConfirmModal: typeof HttpReqChangeConfirmModal;
|
||||
HttpRequest: typeof HttpRequest;
|
||||
HttpRequestOptions: typeof HttpRequestOptions;
|
||||
HttpResponse: typeof HttpResponse;
|
||||
HttpResponseMeta: typeof HttpResponseMeta;
|
||||
HttpSidebar: typeof HttpSidebar;
|
||||
HttpTestResult: typeof HttpTestResult;
|
||||
HttpTestResultEntry: typeof HttpTestResultEntry;
|
||||
HttpTestResultEnv: typeof HttpTestResultEnv;
|
||||
HttpTestResultReport: typeof HttpTestResultReport;
|
||||
HttpTests: typeof HttpTests;
|
||||
HttpURLEncodedParams: typeof HttpURLEncodedParams;
|
||||
LensesHeadersRenderer: typeof LensesHeadersRenderer;
|
||||
LensesHeadersRendererEntry: typeof LensesHeadersRendererEntry;
|
||||
LensesResponseBodyRenderer: typeof LensesResponseBodyRenderer;
|
||||
LensesRenderersHTMLLensRenderer: typeof LensesRenderersHTMLLensRenderer;
|
||||
LensesRenderersImageLensRenderer: typeof LensesRenderersImageLensRenderer;
|
||||
LensesRenderersJSONLensRenderer: typeof LensesRenderersJSONLensRenderer;
|
||||
LensesRenderersPDFLensRenderer: typeof LensesRenderersPDFLensRenderer;
|
||||
LensesRenderersRawLensRenderer: typeof LensesRenderersRawLensRenderer;
|
||||
LensesRenderersXMLLensRenderer: typeof LensesRenderersXMLLensRenderer;
|
||||
ProfilePicture: typeof ProfilePicture;
|
||||
ProfileShortcode: typeof ProfileShortcode;
|
||||
RealtimeCommunication: typeof RealtimeCommunication;
|
||||
RealtimeLog: typeof RealtimeLog;
|
||||
RealtimeLogEntry: typeof RealtimeLogEntry;
|
||||
SmartAccentModePicker: typeof SmartAccentModePicker;
|
||||
SmartAnchor: typeof SmartAnchor;
|
||||
SmartAutoComplete: typeof SmartAutoComplete;
|
||||
SmartChangeLanguage: typeof SmartChangeLanguage;
|
||||
SmartCheckbox: typeof SmartCheckbox;
|
||||
SmartColorModePicker: typeof SmartColorModePicker;
|
||||
SmartConfirmModal: typeof SmartConfirmModal;
|
||||
SmartEnvInput: typeof SmartEnvInput;
|
||||
SmartExpand: typeof SmartExpand;
|
||||
SmartFileChip: typeof SmartFileChip;
|
||||
SmartFontSizePicker: typeof SmartFontSizePicker;
|
||||
SmartIcon: typeof SmartIcon;
|
||||
SmartIntersection: typeof SmartIntersection;
|
||||
SmartItem: typeof SmartItem;
|
||||
SmartLoadingIndicator: typeof SmartLoadingIndicator;
|
||||
SmartModal: typeof SmartModal;
|
||||
SmartProgressRing: typeof SmartProgressRing;
|
||||
SmartRadio: typeof SmartRadio;
|
||||
SmartRadioGroup: typeof SmartRadioGroup;
|
||||
SmartSpinner: typeof SmartSpinner;
|
||||
SmartTab: typeof SmartTab;
|
||||
SmartTabs: typeof SmartTabs;
|
||||
SmartToggle: typeof SmartToggle;
|
||||
TabPrimary: typeof TabPrimary;
|
||||
TabSecondary: typeof TabSecondary;
|
||||
TeamsAdd: typeof TeamsAdd;
|
||||
TeamsEdit: typeof TeamsEdit;
|
||||
TeamsInvite: typeof TeamsInvite;
|
||||
TeamsModal: typeof TeamsModal;
|
||||
TeamsTeam: typeof TeamsTeam;
|
||||
Teams: typeof Teams;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<header
|
||||
class="flex items-center justify-between flex-1 px-2 py-2 overflow-x-auto overflow-y-hidden space-x-2"
|
||||
>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<ButtonSecondary
|
||||
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
||||
:label="t('app.name')"
|
||||
to="/"
|
||||
/>
|
||||
<AppGitHubStarButton class="mt-1.5 transition <sm:hidden" />
|
||||
</div>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<ButtonSecondary
|
||||
v-if="showInstallButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('header.install_pwa')"
|
||||
:icon="IconDownload"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="installPWA()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||
:icon="IconSearch"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${
|
||||
mdAndLarger ? t('support.title') : t('app.options')
|
||||
} <kbd>?</kbd>`"
|
||||
:icon="IconLifeBuoy"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.support.toggle')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="currentUser === null"
|
||||
:icon="IconUploadCloud"
|
||||
:label="t('header.save_workspace')"
|
||||
filled
|
||||
class="hidden md:flex"
|
||||
@click="showLogin = true"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
v-if="currentUser === null"
|
||||
:label="t('header.login')"
|
||||
@click="showLogin = true"
|
||||
/>
|
||||
<div v-else class="inline-flex items-center space-x-2">
|
||||
<ButtonPrimary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.invite_tooltip')"
|
||||
:label="t('team.invite')"
|
||||
:icon="IconUserPlus"
|
||||
class="!bg-green-500 !bg-opacity-15 !text-green-500 !hover:bg-opacity-10 !hover:bg-green-400 !hover:text-green-600"
|
||||
@click="showTeamsModal = true"
|
||||
/>
|
||||
<span class="px-2">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ProfilePicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="currentUser.displayName"
|
||||
:title="currentUser.displayName"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<ProfilePicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="currentUser.displayName"
|
||||
:initial="currentUser.displayName"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col px-2 text-tiny">
|
||||
<span class="inline-flex font-semibold truncate">
|
||||
{{ currentUser.displayName }}
|
||||
</span>
|
||||
<span class="inline-flex truncate text-secondaryLight">
|
||||
{{ currentUser.email }}
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.enter="profile.$el.click()"
|
||||
@keyup.s="settings.$el.click()"
|
||||
@keyup.l="logout.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="profile"
|
||||
to="/profile"
|
||||
:icon="IconUser"
|
||||
:label="t('navigation.profile')"
|
||||
:shortcut="['↩']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="settings"
|
||||
to="/settings"
|
||||
:icon="IconSettings"
|
||||
:label="t('navigation.settings')"
|
||||
:shortcut="['S']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<FirebaseLogout
|
||||
ref="logout"
|
||||
:shortcut="['L']"
|
||||
@confirm-logout="hide()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<AppAnnouncement v-if="!network.isOnline" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconSearch from "~icons/lucide/search"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { probableUser$ } from "@helpers/fb/auth"
|
||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
/**
|
||||
* Once the PWA code is initialized, this holds a method
|
||||
* that can be called to show the user the installation
|
||||
* prompt.
|
||||
*/
|
||||
|
||||
const showInstallButton = computed(() => !!pwaDefferedPrompt.value)
|
||||
|
||||
const showLogin = ref(false)
|
||||
const showTeamsModal = ref(false)
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
|
||||
const network = reactive(useNetwork())
|
||||
|
||||
const currentUser = useReadonlyStream(probableUser$, null)
|
||||
|
||||
onMounted(() => {
|
||||
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
|
||||
if (!cookiesAllowed) {
|
||||
toast.show(`${t("app.we_use_cookies")}`, {
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.learn_more")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
window.open("https://docs.hoppscotch.io/privacy", "_blank")?.focus()
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `${t("action.dismiss")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const profile = ref<any | null>(null)
|
||||
const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
</script>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('folder.new')"
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAddFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addFolder"
|
||||
/>
|
||||
<label for="selectLabelAddFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addFolder"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
folder: { type: Object, default: () => ({}) },
|
||||
folderPath: { type: String, default: null },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
loadingState: Boolean,
|
||||
},
|
||||
emits: ["hide-modal", "add-folder"],
|
||||
setup() {
|
||||
return {
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(isShowing: boolean) {
|
||||
if (!isShowing) this.name = null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addFolder() {
|
||||
if (!this.name) {
|
||||
this.toast.error(this.t("folder.invalid_name"))
|
||||
return
|
||||
}
|
||||
this.$emit("add-folder", {
|
||||
name: this.name,
|
||||
folder: this.folder,
|
||||
path: this.folderPath || `${this.collectionIndex}`,
|
||||
})
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<div v-show="show">
|
||||
<SmartTabs
|
||||
:id="'collections_tab'"
|
||||
v-model="selectedCollectionTab"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
:id="'my-collections'"
|
||||
:label="`${t('collection.my_collections')}`"
|
||||
/>
|
||||
<SmartTab
|
||||
v-if="currentUser"
|
||||
:id="'team-collections'"
|
||||
:label="`${t('collection.team_collections')}`"
|
||||
>
|
||||
<SmartIntersection @intersecting="onTeamSelectIntersect">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
placement="bottom"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('collection.select_team')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam"
|
||||
:icon="IconUsers"
|
||||
:label="collectionsType.selectedTeam.name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="`${t('collection.select_team')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
v-for="(team, index) in myTeams"
|
||||
:key="`team-${index}`"
|
||||
:label="team.name"
|
||||
:info-icon="
|
||||
team.id === collectionsType.selectedTeam?.id
|
||||
? IconDone
|
||||
: null
|
||||
"
|
||||
:active-info-icon="
|
||||
team.id === collectionsType.selectedTeam?.id
|
||||
"
|
||||
:icon="IconUsers"
|
||||
@click="
|
||||
() => {
|
||||
updateSelectedTeam(team)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</SmartIntersection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import { ref, watch } from "vue"
|
||||
import { GetMyTeamsQuery, Team } from "~/helpers/backend/graphql"
|
||||
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
|
||||
type TeamData = GetMyTeamsQuery["myTeams"][number]
|
||||
|
||||
type CollectionTabs = "my-collections" | "team-collections"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const selectedCollectionTab = ref<CollectionTabs>("my-collections")
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
collectionsType: {
|
||||
type: "my-collections" | "team-collections"
|
||||
selectedTeam: Team | undefined
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update-collection-type", tabID: string): void
|
||||
(e: "update-selected-team", team: TeamData | undefined): void
|
||||
}>()
|
||||
|
||||
const currentUser = useReadonlyStream(currentUserInfo$, null)
|
||||
|
||||
const adapter = new TeamListAdapter(true)
|
||||
const myTeams = useReadonlyStream(adapter.teamList$, null)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
let teamListFetched = false
|
||||
|
||||
watch(myTeams, (teams) => {
|
||||
if (teams && !teamListFetched) {
|
||||
teamListFetched = true
|
||||
if (REMEMBERED_TEAM_ID.value && currentUser) {
|
||||
const team = teams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||
if (team) updateSelectedTeam(team)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onLoggedIn(() => {
|
||||
adapter.initialize()
|
||||
})
|
||||
|
||||
const onTeamSelectIntersect = () => {
|
||||
// Load team data as soon as intersection
|
||||
adapter.fetchList()
|
||||
}
|
||||
|
||||
const updateCollectionsType = (tabID: string) => {
|
||||
emit("update-collection-type", tabID)
|
||||
}
|
||||
|
||||
const updateSelectedTeam = (team: TeamData | undefined) => {
|
||||
REMEMBERED_TEAM_ID.value = team?.id
|
||||
emit("update-selected-team", team)
|
||||
}
|
||||
|
||||
watch(selectedCollectionTab, (newValue: string) => {
|
||||
updateCollectionsType(newValue)
|
||||
})
|
||||
</script>
|
||||
@@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.edit_request')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEditReq"
|
||||
v-model="requestUpdateData.name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequest"
|
||||
/>
|
||||
<label for="selectLabelEditReq">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
editingRequestName: { type: String, default: null },
|
||||
loadingState: Boolean,
|
||||
},
|
||||
emits: ["submit", "hide-modal"],
|
||||
setup() {
|
||||
return {
|
||||
t: useI18n(),
|
||||
toast: useToast(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
requestUpdateData: {
|
||||
name: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editingRequestName(val) {
|
||||
this.requestUpdateData.name = val
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveRequest() {
|
||||
if (!this.requestUpdateData.name) {
|
||||
this.toast.error(this.t("request.invalid_name"))
|
||||
return
|
||||
}
|
||||
this.$emit("submit", this.requestUpdateData)
|
||||
},
|
||||
hideModal() {
|
||||
this.requestUpdateData = { name: null }
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,400 +0,0 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('collection.save_as')}`"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelSaveReq"
|
||||
v-model="requestName"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequestAs"
|
||||
/>
|
||||
<label for="selectLabelSaveReq">
|
||||
{{ t("request.name") }}
|
||||
</label>
|
||||
</div>
|
||||
<label class="p-4">
|
||||
{{ t("collection.select_location") }}
|
||||
</label>
|
||||
<CollectionsGraphql
|
||||
v-if="mode === 'graphql'"
|
||||
:show-coll-actions="false"
|
||||
:picked="picked"
|
||||
:saving-mode="true"
|
||||
@select="onSelect"
|
||||
/>
|
||||
<Collections
|
||||
v-else
|
||||
:picked="picked"
|
||||
:save-request="true"
|
||||
@select="onSelect"
|
||||
@update-collection="updateColl"
|
||||
@update-coll-type="onUpdateCollType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveRequestAs"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { HoppGQLRequest, isHoppRESTRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
editGraphqlRequest,
|
||||
editRESTRequest,
|
||||
saveGraphqlRequestAs,
|
||||
saveRESTRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||
import {
|
||||
getRESTRequest,
|
||||
setRESTSaveContext,
|
||||
useRESTRequestName,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
CreateRequestInCollectionDocument,
|
||||
UpdateRequestDocument,
|
||||
} from "~/helpers/backend/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type CollectionType =
|
||||
| {
|
||||
type: "my-collections"
|
||||
}
|
||||
| {
|
||||
type: "team-collections"
|
||||
// TODO: Figure this type out
|
||||
selectedTeam: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
type Picked =
|
||||
| {
|
||||
pickedType: "my-request"
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}
|
||||
| {
|
||||
pickedType: "my-folder"
|
||||
folderPath: string
|
||||
}
|
||||
| {
|
||||
pickedType: "my-collection"
|
||||
collectionIndex: number
|
||||
}
|
||||
| {
|
||||
pickedType: "teams-request"
|
||||
requestID: string
|
||||
}
|
||||
| {
|
||||
pickedType: "teams-folder"
|
||||
folderID: string
|
||||
}
|
||||
| {
|
||||
pickedType: "teams-collection"
|
||||
collectionID: string
|
||||
}
|
||||
| {
|
||||
pickedType: "gql-my-request"
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}
|
||||
| {
|
||||
pickedType: "gql-my-folder"
|
||||
folderPath: string
|
||||
}
|
||||
| {
|
||||
pickedType: "gql-my-collection"
|
||||
collectionIndex: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
mode: "rest" | "graphql"
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
// TODO: Use a better implementation with computed ?
|
||||
// This implementation can't work across updates to mode prop (which won't happen tho)
|
||||
const requestName =
|
||||
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
|
||||
|
||||
const requestData = reactive({
|
||||
name: requestName,
|
||||
collectionIndex: undefined as number | undefined,
|
||||
folderName: undefined as number | undefined,
|
||||
requestIndex: undefined as number | undefined,
|
||||
})
|
||||
|
||||
const collectionsType = ref<CollectionType>({
|
||||
type: "my-collections",
|
||||
})
|
||||
|
||||
// TODO: Figure this type out
|
||||
const picked = ref<Picked | null>(null)
|
||||
|
||||
// Resets
|
||||
watch(
|
||||
() => requestData.collectionIndex,
|
||||
() => {
|
||||
requestData.folderName = undefined
|
||||
requestData.requestIndex = undefined
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => requestData.folderName,
|
||||
() => {
|
||||
requestData.requestIndex = undefined
|
||||
}
|
||||
)
|
||||
|
||||
// All the methods
|
||||
const onUpdateCollType = (newCollType: CollectionType) => {
|
||||
collectionsType.value = newCollType
|
||||
}
|
||||
|
||||
const onSelect = ({ picked: pickedVal }: { picked: Picked | null }) => {
|
||||
picked.value = pickedVal
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
picked.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const saveRequestAs = async () => {
|
||||
if (!requestName.value) {
|
||||
toast.error(`${t("error.empty_req_name")}`)
|
||||
return
|
||||
}
|
||||
if (picked.value === null) {
|
||||
toast.error(`${t("collection.select")}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Clone Deep because objects are shared by reference so updating
|
||||
// just one bit will update other referenced shared instances
|
||||
const requestUpdated =
|
||||
props.mode === "rest"
|
||||
? cloneDeep(getRESTRequest())
|
||||
: cloneDeep(getGQLSession().request)
|
||||
|
||||
// // Filter out all REST file inputs
|
||||
// if (this.mode === "rest" && requestUpdated.bodyParams) {
|
||||
// requestUpdated.bodyParams = requestUpdated.bodyParams.map((param) =>
|
||||
// param?.value?.[0] instanceof File ? { ...param, value: "" } : param
|
||||
// )
|
||||
// }
|
||||
|
||||
if (picked.value.pickedType === "my-request") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
editRESTRequest(
|
||||
picked.value.folderPath,
|
||||
picked.value.requestIndex,
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: picked.value.requestIndex,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-folder") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
const insertionIndex = saveRESTRequestAs(
|
||||
picked.value.folderPath,
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: insertionIndex,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-collection") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
const insertionIndex = saveRESTRequestAs(
|
||||
`${picked.value.collectionIndex}`,
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: `${picked.value.collectionIndex}`,
|
||||
requestIndex: insertionIndex,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "teams-request") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
if (collectionsType.value.type !== "team-collections")
|
||||
throw new Error("Collections Type mismatch")
|
||||
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: picked.value.requestID,
|
||||
data: {
|
||||
request: JSON.stringify(requestUpdated),
|
||||
title: requestUpdated.name,
|
||||
},
|
||||
})().then((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
throw new Error(`${result.left}`)
|
||||
} else {
|
||||
requestSaved()
|
||||
}
|
||||
})
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: picked.value.requestID,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
} else if (picked.value.pickedType === "teams-folder") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
if (collectionsType.value.type !== "team-collections")
|
||||
throw new Error("Collections Type mismatch")
|
||||
|
||||
const result = await runMutation(CreateRequestInCollectionDocument, {
|
||||
collectionID: picked.value.folderID,
|
||||
data: {
|
||||
request: JSON.stringify(requestUpdated),
|
||||
teamID: collectionsType.value.selectedTeam.id,
|
||||
title: requestUpdated.name,
|
||||
},
|
||||
})()
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
console.error(result.left)
|
||||
} else {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: result.right.createRequestInCollection.id,
|
||||
teamID: collectionsType.value.selectedTeam.id,
|
||||
collectionID: picked.value.folderID,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
} else if (picked.value.pickedType === "teams-collection") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
if (collectionsType.value.type !== "team-collections")
|
||||
throw new Error("Collections Type mismatch")
|
||||
|
||||
const result = await runMutation(CreateRequestInCollectionDocument, {
|
||||
collectionID: picked.value.collectionID,
|
||||
data: {
|
||||
title: requestUpdated.name,
|
||||
request: JSON.stringify(requestUpdated),
|
||||
teamID: collectionsType.value.selectedTeam.id,
|
||||
},
|
||||
})()
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
console.error(result.left)
|
||||
} else {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: result.right.createRequestInCollection.id,
|
||||
teamID: collectionsType.value.selectedTeam.id,
|
||||
collectionID: picked.value.collectionID,
|
||||
req: cloneDeep(requestUpdated),
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
} else if (picked.value.pickedType === "gql-my-request") {
|
||||
// TODO: Check for GQL request ?
|
||||
editGraphqlRequest(
|
||||
picked.value.folderPath,
|
||||
picked.value.requestIndex,
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-folder") {
|
||||
// TODO: Check for GQL request ?
|
||||
saveGraphqlRequestAs(
|
||||
picked.value.folderPath,
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-collection") {
|
||||
// TODO: Check for GQL request ?
|
||||
saveGraphqlRequestAs(
|
||||
`${picked.value.collectionIndex}`,
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
}
|
||||
|
||||
const requestSaved = () => {
|
||||
toast.success(`${t("request.added")}`)
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const updateColl = (ev: CollectionType["type"]) => {
|
||||
collectionsType.value.type = ev
|
||||
}
|
||||
</script>
|
||||
@@ -1,961 +0,0 @@
|
||||
<template>
|
||||
<div :class="{ 'rounded border border-divider': saveRequest }">
|
||||
<div
|
||||
class="sticky z-10 flex flex-col border-b rounded-t bg-primary border-dividerLight"
|
||||
:style="
|
||||
saveRequest ? 'top: calc(-1.35 * var(--font-size-body))' : 'top: 0'
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col border-b border-dividerLight">
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="py-2 pl-4 pr-2 bg-transparent"
|
||||
:disabled="collectionsType.type == 'team-collections'"
|
||||
/>
|
||||
</div>
|
||||
<CollectionsChooseType
|
||||
:collections-type="collectionsType"
|
||||
:show="showTeamCollections"
|
||||
@update-collection-type="updateCollectionType"
|
||||
@update-selected-team="updateSelectedTeam"
|
||||
/>
|
||||
<div class="flex justify-between flex-1">
|
||||
<ButtonSecondary
|
||||
v-if="
|
||||
collectionsType.type == 'team-collections' &&
|
||||
(collectionsType.selectedTeam == undefined ||
|
||||
collectionsType.selectedTeam.myRole == 'VIEWER')
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
class="!rounded-none"
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
class="!rounded-none"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<span class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/collections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="
|
||||
collectionsType.type == 'team-collections' &&
|
||||
collectionsType.selectedTeam == undefined
|
||||
"
|
||||
:icon="IconArchive"
|
||||
:title="t('modal.import_export')"
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<component
|
||||
:is="
|
||||
collectionsType.type == 'my-collections'
|
||||
? 'CollectionsMyCollection'
|
||||
: 'CollectionsTeamsCollection'
|
||||
"
|
||||
v-for="(collection, index) in filteredCollections"
|
||||
:key="`collection-${index}`"
|
||||
:collection-index="parseInt(index)"
|
||||
:collection="collection"
|
||||
:is-filtered="filterText.length > 0"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
:loading-collection-i-ds="loadingCollectionIDs"
|
||||
@edit-collection="editCollection(collection, index)"
|
||||
@add-request="addRequest($event)"
|
||||
@add-folder="addFolder($event)"
|
||||
@edit-folder="editFolder($event)"
|
||||
@edit-request="editRequest($event)"
|
||||
@duplicate-request="duplicateRequest($event)"
|
||||
@update-team-collections="updateTeamCollections"
|
||||
@select-collection="$emit('use-collection', collection)"
|
||||
@unselect-collection="$emit('remove-collection', collection)"
|
||||
@select="$emit('select', $event)"
|
||||
@expand-collection="expandCollection"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-request="removeRequest"
|
||||
@remove-folder="removeFolder"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="loadingCollectionIDs.includes('root')"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredCollections.length === 0 && filterText.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="t('empty.collections')"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
v-if="
|
||||
collectionsType.type == 'team-collections' &&
|
||||
(collectionsType.selectedTeam == undefined ||
|
||||
collectionsType.selectedTeam.myRole == 'VIEWER')
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
class="mb-4"
|
||||
filled
|
||||
outline
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
class="mb-4"
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="filterText.length !== 0 && filteredCollections.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") }} "{{ filterText }}"
|
||||
</span>
|
||||
</div>
|
||||
<CollectionsAdd
|
||||
:show="showModalAdd"
|
||||
:loading-state="modalLoadingState"
|
||||
@submit="addNewRootCollection"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
/>
|
||||
<CollectionsEdit
|
||||
:show="showModalEdit"
|
||||
:editing-collection-name="
|
||||
editingCollection
|
||||
? editingCollection.name || editingCollection.title
|
||||
: ''
|
||||
"
|
||||
:loading-state="modalLoadingState"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
@submit="updateEditingCollection"
|
||||
/>
|
||||
<CollectionsAddRequest
|
||||
:show="showModalAddRequest"
|
||||
:folder="editingFolder"
|
||||
:folder-path="editingFolderPath"
|
||||
:loading-state="modalLoadingState"
|
||||
@add-request="onAddRequest($event)"
|
||||
@hide-modal="displayModalAddRequest(false)"
|
||||
/>
|
||||
<CollectionsAddFolder
|
||||
:show="showModalAddFolder"
|
||||
:folder="editingFolder"
|
||||
:folder-path="editingFolderPath"
|
||||
:loading-state="modalLoadingState"
|
||||
@add-folder="onAddFolder($event)"
|
||||
@hide-modal="displayModalAddFolder(false)"
|
||||
/>
|
||||
<CollectionsEditFolder
|
||||
:show="showModalEditFolder"
|
||||
:editing-folder-name="
|
||||
editingFolder ? editingFolder.name || editingFolder.title : ''
|
||||
"
|
||||
:loading-state="modalLoadingState"
|
||||
@submit="updateEditingFolder"
|
||||
@hide-modal="displayModalEditFolder(false)"
|
||||
/>
|
||||
<CollectionsEditRequest
|
||||
:show="showModalEditRequest"
|
||||
:editing-request-name="editingRequest ? editingRequest.name : ''"
|
||||
:loading-state="modalLoadingState"
|
||||
@submit="updateEditingRequest"
|
||||
@hide-modal="displayModalEditRequest(false)"
|
||||
/>
|
||||
<CollectionsImportExport
|
||||
:show="showModalImportExport"
|
||||
:collections-type="collectionsType"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
@update-team-collections="updateTeamCollections"
|
||||
/>
|
||||
<SmartConfirmModal
|
||||
:show="showConfirmModal"
|
||||
:title="confirmModalTitle"
|
||||
:loading-state="modalLoadingState"
|
||||
@hide-modal="showConfirmModal = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { defineComponent, markRaw } from "vue"
|
||||
import { makeCollection } from "@hoppscotch/data"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import * as E from "fp-ts/Either"
|
||||
import CollectionsMyCollection from "./my/Collection.vue"
|
||||
import CollectionsTeamsCollection from "./teams/Collection.vue"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import {
|
||||
restCollections$,
|
||||
addRESTCollection,
|
||||
editRESTCollection,
|
||||
addRESTFolder,
|
||||
removeRESTCollection,
|
||||
removeRESTFolder,
|
||||
editRESTFolder,
|
||||
removeRESTRequest,
|
||||
editRESTRequest,
|
||||
saveRESTRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import {
|
||||
setRESTRequest,
|
||||
getRESTRequest,
|
||||
getRESTSaveContext,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
CreateChildCollectionDocument,
|
||||
CreateNewRootCollectionDocument,
|
||||
CreateRequestInCollectionDocument,
|
||||
DeleteCollectionDocument,
|
||||
DeleteRequestDocument,
|
||||
RenameCollectionDocument,
|
||||
UpdateRequestDocument,
|
||||
} from "~/helpers/backend/graphql"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CollectionsMyCollection,
|
||||
CollectionsTeamsCollection,
|
||||
},
|
||||
props: {
|
||||
saveRequest: Boolean,
|
||||
picked: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: [
|
||||
"update-collection",
|
||||
"update-coll-type",
|
||||
"update-team-collections",
|
||||
"select-request",
|
||||
"select",
|
||||
"use-collection",
|
||||
"remove-collection",
|
||||
],
|
||||
setup() {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
return {
|
||||
subscribeTo: subscribeToStream,
|
||||
|
||||
collections: useReadonlyStream(restCollections$, [], "deep"),
|
||||
currentUser: useReadonlyStream(currentUser$, null),
|
||||
colorMode: useColorMode(),
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
IconArchive: markRaw(IconArchive),
|
||||
IconHelpCircle: markRaw(IconHelpCircle),
|
||||
IconPlus: markRaw(IconPlus),
|
||||
showModalAdd: false,
|
||||
showModalEdit: false,
|
||||
showModalImportExport: false,
|
||||
showModalAddRequest: false,
|
||||
showModalAddFolder: false,
|
||||
showModalEditFolder: false,
|
||||
showModalEditRequest: false,
|
||||
showConfirmModal: false,
|
||||
modalLoadingState: false,
|
||||
editingCollection: undefined,
|
||||
editingCollectionIndex: undefined,
|
||||
editingCollectionID: undefined,
|
||||
editingFolder: undefined,
|
||||
editingFolderName: undefined,
|
||||
editingFolderIndex: undefined,
|
||||
editingFolderPath: undefined,
|
||||
editingRequest: undefined,
|
||||
editingRequestIndex: undefined,
|
||||
confirmModalTitle: undefined,
|
||||
filterText: "",
|
||||
collectionsType: {
|
||||
type: "my-collections",
|
||||
selectedTeam: undefined,
|
||||
},
|
||||
teamCollectionAdapter: new TeamCollectionAdapter(null),
|
||||
teamCollectionsNew: [],
|
||||
loadingCollectionIDs: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showTeamCollections() {
|
||||
if (this.currentUser == null) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
filteredCollections() {
|
||||
const collections =
|
||||
this.collectionsType.type === "my-collections"
|
||||
? this.collections
|
||||
: this.teamCollectionsNew
|
||||
|
||||
if (!this.filterText) {
|
||||
return collections
|
||||
}
|
||||
|
||||
if (this.collectionsType.type === "team-collections") {
|
||||
return []
|
||||
}
|
||||
|
||||
const filterText = this.filterText.toLowerCase()
|
||||
const filteredCollections = []
|
||||
|
||||
for (const collection of collections) {
|
||||
const filteredRequests = []
|
||||
const filteredFolders = []
|
||||
for (const request of collection.requests) {
|
||||
if (request.name.toLowerCase().includes(filterText))
|
||||
filteredRequests.push(request)
|
||||
}
|
||||
for (const folder of this.collectionsType.type === "team-collections"
|
||||
? collection.children
|
||||
: collection.folders) {
|
||||
const filteredFolderRequests = []
|
||||
for (const request of folder.requests) {
|
||||
if (request.name.toLowerCase().includes(filterText))
|
||||
filteredFolderRequests.push(request)
|
||||
}
|
||||
if (filteredFolderRequests.length > 0) {
|
||||
const filteredFolder = Object.assign({}, folder)
|
||||
filteredFolder.requests = filteredFolderRequests
|
||||
filteredFolders.push(filteredFolder)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
filteredRequests.length + filteredFolders.length > 0 ||
|
||||
collection.name.toLowerCase().includes(filterText)
|
||||
) {
|
||||
const filteredCollection = Object.assign({}, collection)
|
||||
filteredCollection.requests = filteredRequests
|
||||
filteredCollection.folders = filteredFolders
|
||||
filteredCollections.push(filteredCollection)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCollections
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"collectionsType.type": function emitstuff() {
|
||||
this.$emit("update-collection", this.$data.collectionsType.type)
|
||||
},
|
||||
"collectionsType.selectedTeam"(value) {
|
||||
if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
|
||||
},
|
||||
currentUser(newValue) {
|
||||
if (!newValue) this.updateCollectionType("my-collections")
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.teamCollectionAdapter.unsubscribeSubscriptions()
|
||||
},
|
||||
mounted() {
|
||||
this.subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
|
||||
this.teamCollectionsNew = cloneDeep(colls)
|
||||
})
|
||||
this.subscribeTo(
|
||||
this.teamCollectionAdapter.loadingCollections$,
|
||||
(collectionsIDs) => {
|
||||
this.loadingCollectionIDs = collectionsIDs
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
updateTeamCollections() {
|
||||
// TODO: Remove this at some point
|
||||
},
|
||||
updateSelectedTeam(newSelectedTeam) {
|
||||
this.collectionsType.selectedTeam = newSelectedTeam
|
||||
this.$emit("update-coll-type", this.collectionsType)
|
||||
},
|
||||
updateCollectionType(newCollectionType) {
|
||||
this.collectionsType.type = newCollectionType
|
||||
this.$emit("update-coll-type", this.collectionsType)
|
||||
},
|
||||
// Intented to be called by the CollectionAdd modal submit event
|
||||
addNewRootCollection(name) {
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
addRESTCollection(
|
||||
makeCollection({
|
||||
name,
|
||||
folders: [],
|
||||
requests: [],
|
||||
})
|
||||
)
|
||||
|
||||
this.displayModalAdd(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
runMutation(CreateNewRootCollectionDocument, {
|
||||
title: name,
|
||||
teamID: this.collectionsType.selectedTeam.id,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
if (E.isLeft(result)) {
|
||||
if (result.left.error === "team_coll/short_title")
|
||||
this.toast.error(this.t("collection.name_length_insufficient"))
|
||||
else this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("collection.created"))
|
||||
this.displayModalAdd(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
// Intented to be called by CollectionEdit modal submit event
|
||||
updateEditingCollection(newName) {
|
||||
if (!newName) {
|
||||
this.toast.error(this.t("collection.invalid_name"))
|
||||
return
|
||||
}
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
const collectionUpdated = {
|
||||
...this.editingCollection,
|
||||
name: newName,
|
||||
}
|
||||
|
||||
editRESTCollection(this.editingCollectionIndex, collectionUpdated)
|
||||
this.displayModalEdit(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
|
||||
runMutation(RenameCollectionDocument, {
|
||||
collectionID: this.editingCollection.id,
|
||||
newTitle: newName,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("collection.renamed"))
|
||||
this.displayModalEdit(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
// Intended to be called by CollectionEditFolder modal submit event
|
||||
updateEditingFolder(name) {
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
|
||||
this.displayModalEditFolder(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
|
||||
runMutation(RenameCollectionDocument, {
|
||||
collectionID: this.editingFolder.id,
|
||||
newTitle: name,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
if (result.left.error === "team_coll/short_title")
|
||||
this.toast.error(this.t("folder.name_length_insufficient"))
|
||||
else this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("folder.renamed"))
|
||||
this.displayModalEditFolder(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
// Intented to by called by CollectionsEditRequest modal submit event
|
||||
updateEditingRequest(requestUpdateData) {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
|
||||
const requestUpdated = {
|
||||
...this.editingRequest,
|
||||
name: requestUpdateData.name || this.editingRequest.name,
|
||||
}
|
||||
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
// Update REST Session with the updated state
|
||||
if (
|
||||
saveCtx &&
|
||||
saveCtx.originLocation === "user-collection" &&
|
||||
saveCtx.requestIndex === this.editingRequestIndex &&
|
||||
saveCtx.folderPath === this.editingFolderPath
|
||||
) {
|
||||
setRESTRequest({
|
||||
...getRESTRequest(),
|
||||
name: requestUpdateData.name,
|
||||
})
|
||||
}
|
||||
|
||||
editRESTRequest(
|
||||
this.editingFolderPath,
|
||||
this.editingRequestIndex,
|
||||
requestUpdated
|
||||
)
|
||||
this.displayModalEditRequest(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
|
||||
const requestName = requestUpdateData.name || this.editingRequest.name
|
||||
|
||||
// Update REST Session with the updated state
|
||||
if (
|
||||
saveCtx &&
|
||||
saveCtx.originLocation === "team-collection" &&
|
||||
saveCtx.requestID === this.editingRequestIndex
|
||||
) {
|
||||
setRESTRequest({
|
||||
...getRESTRequest(),
|
||||
name: requestUpdateData.name,
|
||||
})
|
||||
}
|
||||
|
||||
runMutation(UpdateRequestDocument, {
|
||||
data: {
|
||||
request: JSON.stringify(requestUpdated),
|
||||
title: requestName,
|
||||
},
|
||||
requestID: this.editingRequestIndex,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("request.renamed"))
|
||||
this.$emit("update-team-collections")
|
||||
this.displayModalEditRequest(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
displayModalAdd(shouldDisplay) {
|
||||
this.showModalAdd = shouldDisplay
|
||||
},
|
||||
displayModalEdit(shouldDisplay) {
|
||||
this.showModalEdit = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalImportExport(shouldDisplay) {
|
||||
this.showModalImportExport = shouldDisplay
|
||||
},
|
||||
displayModalAddRequest(shouldDisplay) {
|
||||
this.showModalAddRequest = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalAddFolder(shouldDisplay) {
|
||||
this.showModalAddFolder = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalEditFolder(shouldDisplay) {
|
||||
this.showModalEditFolder = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalEditRequest(shouldDisplay) {
|
||||
this.showModalEditRequest = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayConfirmModal(shouldDisplay) {
|
||||
this.showConfirmModal = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
editCollection(collection, collectionIndex) {
|
||||
this.$data.editingCollection = collection
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.displayModalEdit(true)
|
||||
},
|
||||
onAddFolder({ name, folder, path }) {
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
addRESTFolder(name, path)
|
||||
this.displayModalAddFolder(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
runMutation(CreateChildCollectionDocument, {
|
||||
childTitle: name,
|
||||
collectionID: folder.id,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
if (E.isLeft(result)) {
|
||||
if (result.left.error === "team_coll/short_title")
|
||||
this.toast.error(this.t("folder.name_length_insufficient"))
|
||||
else this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("folder.created"))
|
||||
this.displayModalAddFolder(false)
|
||||
this.$emit("update-team-collections")
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
addFolder(payload) {
|
||||
const { folder, path } = payload
|
||||
this.$data.editingFolder = folder
|
||||
this.$data.editingFolderPath = path
|
||||
this.displayModalAddFolder(true)
|
||||
},
|
||||
editFolder(payload) {
|
||||
const { collectionIndex, folder, folderIndex, folderPath } = payload
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.$data.editingFolder = folder
|
||||
this.$data.editingFolderIndex = folderIndex
|
||||
this.$data.editingFolderPath = folderPath
|
||||
this.$data.collectionsType = this.collectionsType
|
||||
this.displayModalEditFolder(true)
|
||||
},
|
||||
editRequest(payload) {
|
||||
const {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
folderPath,
|
||||
} = payload
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.$data.editingFolderIndex = folderIndex
|
||||
this.$data.editingFolderName = folderName
|
||||
this.$data.editingRequest = request
|
||||
this.$data.editingRequestIndex = requestIndex
|
||||
this.editingFolderPath = folderPath
|
||||
this.$emit("select-request", requestIndex)
|
||||
this.displayModalEditRequest(true)
|
||||
},
|
||||
resetSelectedData() {
|
||||
this.$data.editingCollection = undefined
|
||||
this.$data.editingCollectionIndex = undefined
|
||||
this.$data.editingCollectionID = undefined
|
||||
this.$data.editingFolder = undefined
|
||||
this.$data.editingFolderPath = undefined
|
||||
this.$data.editingFolderIndex = undefined
|
||||
this.$data.editingRequest = undefined
|
||||
this.$data.editingRequestIndex = undefined
|
||||
|
||||
this.$data.confirmModalTitle = undefined
|
||||
},
|
||||
expandCollection(collectionID) {
|
||||
this.teamCollectionAdapter.expandCollection(collectionID)
|
||||
},
|
||||
removeCollection({ collectionIndex, collectionID }) {
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.$data.editingCollectionID = collectionID
|
||||
this.confirmModalTitle = `${this.t("confirm.remove_collection")}`
|
||||
|
||||
this.displayConfirmModal(true)
|
||||
},
|
||||
onRemoveCollection() {
|
||||
const collectionIndex = this.$data.editingCollectionIndex
|
||||
const collectionID = this.$data.editingCollectionID
|
||||
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
// Cancel pick if picked collection is deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "my-collection" &&
|
||||
this.picked.collectionIndex === collectionIndex
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
|
||||
removeRESTCollection(collectionIndex)
|
||||
|
||||
this.toast.success(this.t("state.deleted"))
|
||||
this.displayConfirmModal(false)
|
||||
} else if (this.collectionsType.type === "team-collections") {
|
||||
this.modalLoadingState = true
|
||||
|
||||
// Cancel pick if picked collection is deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "teams-collection" &&
|
||||
this.picked.collectionID === collectionID
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
|
||||
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
|
||||
runMutation(DeleteCollectionDocument, {
|
||||
collectionID,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("state.deleted"))
|
||||
this.displayConfirmModal(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
removeFolder({ collectionID, folder, folderPath }) {
|
||||
this.$data.editingCollectionID = collectionID
|
||||
this.$data.editingFolder = folder
|
||||
this.$data.editingFolderPath = folderPath
|
||||
this.confirmModalTitle = `${this.t("confirm.remove_folder")}`
|
||||
|
||||
this.displayConfirmModal(true)
|
||||
},
|
||||
onRemoveFolder() {
|
||||
const folder = this.$data.editingFolder
|
||||
const folderPath = this.$data.editingFolderPath
|
||||
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
// Cancel pick if picked folder was deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "my-folder" &&
|
||||
this.picked.folderPath === folderPath
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
removeRESTFolder(folderPath)
|
||||
|
||||
this.toast.success(this.t("state.deleted"))
|
||||
this.displayConfirmModal(false)
|
||||
} else if (this.collectionsType.type === "team-collections") {
|
||||
this.modalLoadingState = true
|
||||
|
||||
// Cancel pick if picked collection folder was deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "teams-folder" &&
|
||||
this.picked.folderID === folder.id
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
|
||||
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
|
||||
runMutation(DeleteCollectionDocument, {
|
||||
collectionID: folder.id,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(`${this.t("state.deleted")}`)
|
||||
this.displayConfirmModal(false)
|
||||
|
||||
this.updateTeamCollections()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRequest({ requestIndex, folderPath }) {
|
||||
this.$data.editingRequestIndex = requestIndex
|
||||
this.$data.editingFolderPath = folderPath
|
||||
this.confirmModalTitle = `${this.t("confirm.remove_request")}`
|
||||
|
||||
this.displayConfirmModal(true)
|
||||
},
|
||||
onRemoveRequest() {
|
||||
const requestIndex = this.$data.editingRequestIndex
|
||||
const folderPath = this.$data.editingFolderPath
|
||||
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
// Cancel pick if the picked item is being deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "my-request" &&
|
||||
this.picked.folderPath === folderPath &&
|
||||
this.picked.requestIndex === requestIndex
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
removeRESTRequest(folderPath, requestIndex)
|
||||
|
||||
this.toast.success(this.t("state.deleted"))
|
||||
this.displayConfirmModal(false)
|
||||
} else if (this.collectionsType.type === "team-collections") {
|
||||
this.modalLoadingState = true
|
||||
// Cancel pick if the picked item is being deleted
|
||||
if (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "teams-request" &&
|
||||
this.picked.requestID === requestIndex
|
||||
) {
|
||||
this.$emit("select", { picked: null })
|
||||
}
|
||||
|
||||
runMutation(DeleteRequestDocument, {
|
||||
requestID: requestIndex,
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
this.toast.success(this.t("state.deleted"))
|
||||
this.displayConfirmModal(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
addRequest(payload) {
|
||||
// TODO: check if the request being worked on
|
||||
// is being overwritten (selected or not)
|
||||
const { folder, path } = payload
|
||||
this.$data.editingFolder = folder
|
||||
this.$data.editingFolderPath = path
|
||||
this.displayModalAddRequest(true)
|
||||
},
|
||||
onAddRequest({ name, folder, path }) {
|
||||
const newRequest = {
|
||||
...cloneDeep(getRESTRequest()),
|
||||
name,
|
||||
}
|
||||
|
||||
if (this.collectionsType.type === "my-collections") {
|
||||
const insertionIndex = saveRESTRequestAs(path, newRequest)
|
||||
// point to it
|
||||
setRESTRequest(newRequest, {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: insertionIndex,
|
||||
})
|
||||
|
||||
this.displayModalAddRequest(false)
|
||||
} else if (
|
||||
this.collectionsType.type === "team-collections" &&
|
||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||
) {
|
||||
this.modalLoadingState = true
|
||||
runMutation(CreateRequestInCollectionDocument, {
|
||||
collectionID: folder.id,
|
||||
data: {
|
||||
request: JSON.stringify(newRequest),
|
||||
teamID: this.collectionsType.selectedTeam.id,
|
||||
title: name,
|
||||
},
|
||||
})().then((result) => {
|
||||
this.modalLoadingState = false
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
console.error(result.left.error)
|
||||
} else {
|
||||
const { createRequestInCollection } = result.right
|
||||
// point to it
|
||||
setRESTRequest(newRequest, {
|
||||
originLocation: "team-collection",
|
||||
requestID: createRequestInCollection.id,
|
||||
collectionID: createRequestInCollection.collection.id,
|
||||
teamID: createRequestInCollection.collection.team.id,
|
||||
})
|
||||
this.displayModalAddRequest(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
duplicateRequest({ folderPath, request, collectionID }) {
|
||||
if (this.collectionsType.type === "team-collections") {
|
||||
const newReq = {
|
||||
...cloneDeep(request),
|
||||
name: `${request.name} - ${this.t("action.duplicate")}`,
|
||||
}
|
||||
|
||||
// Error handling ?
|
||||
runMutation(CreateRequestInCollectionDocument, {
|
||||
collectionID,
|
||||
data: {
|
||||
request: JSON.stringify(newReq),
|
||||
teamID: this.collectionsType.selectedTeam.id,
|
||||
title: `${request.name} - ${this.t("action.duplicate")}`,
|
||||
},
|
||||
})()
|
||||
} else if (this.collectionsType.type === "my-collections") {
|
||||
saveRESTRequestAs(folderPath, {
|
||||
...cloneDeep(request),
|
||||
name: `${request.name} - ${this.t("action.duplicate")}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
resolveConfirmModal(title) {
|
||||
if (title === `${this.t("confirm.remove_collection")}`)
|
||||
this.onRemoveCollection()
|
||||
else if (title === `${this.t("confirm.remove_request")}`)
|
||||
this.onRemoveRequest()
|
||||
else if (title === `${this.t("confirm.remove_folder")}`)
|
||||
this.onRemoveFolder()
|
||||
else {
|
||||
console.error(
|
||||
`Confirm modal title ${title} is not handled by the component`
|
||||
)
|
||||
this.toast.error(this.t("error.something_went_wrong"))
|
||||
this.displayConfirmModal(false)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
// request inside folder is not being deleted, you dumb fuck
|
||||
</script>
|
||||
@@ -1,354 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<component
|
||||
:is="getCollectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collection.name }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="
|
||||
$emit('add-request', {
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="
|
||||
$emit('add-folder', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction.$el.click()"
|
||||
@keyup.n="folderAction.$el.click()"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.x="exportAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-request', {
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-folder', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
@click="
|
||||
() => {
|
||||
exportCollection()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeCollection()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsMyFolder
|
||||
v-for="(folder, index) in collection.folders"
|
||||
:key="`folder-${index}`"
|
||||
:folder="folder"
|
||||
:folder-index="index"
|
||||
:folder-path="`${collectionIndex}/${index}`"
|
||||
:collection-index="collectionIndex"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:is-filtered="isFiltered"
|
||||
:picked="picked"
|
||||
@add-request="$emit('add-request', $event)"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@remove-folder="$emit('remove-folder', $event)"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:folder-path="`${collectionIndex}`"
|
||||
:request-index="index"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
(collection.folders == undefined ||
|
||||
collection.folders.length === 0) &&
|
||||
(collection.requests == undefined ||
|
||||
collection.requests.length === 0)
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconFilePlus from "~icons/lucide/file-plus"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { defineComponent, ref, markRaw } from "vue"
|
||||
import { moveRESTRequest } from "~/newstore/collections"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collectionIndex: { type: Number, default: null },
|
||||
collection: { type: Object, default: () => ({}) },
|
||||
isFiltered: Boolean,
|
||||
saveRequest: Boolean,
|
||||
collectionsType: { type: Object, default: () => ({}) },
|
||||
picked: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: [
|
||||
"select",
|
||||
"expand-collection",
|
||||
"add-collection",
|
||||
"remove-collection",
|
||||
"add-folder",
|
||||
"add-request",
|
||||
"edit-folder",
|
||||
"edit-request",
|
||||
"duplicate-request",
|
||||
"remove-folder",
|
||||
"remove-request",
|
||||
"select-collection",
|
||||
"unselect-collection",
|
||||
"edit-collection",
|
||||
],
|
||||
setup() {
|
||||
return {
|
||||
colorMode: useColorMode(),
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
|
||||
// Template refs
|
||||
tippyActions: ref<any | null>(null),
|
||||
options: ref<any | null>(null),
|
||||
requestAction: ref<any | null>(null),
|
||||
folderAction: ref<any | null>(null),
|
||||
edit: ref<any | null>(null),
|
||||
deleteAction: ref<any | null>(null),
|
||||
exportAction: ref<any | null>(null),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
IconCircle: markRaw(IconCircle),
|
||||
IconCheckCircle: markRaw(IconCheckCircle),
|
||||
IconFilePlus: markRaw(IconFilePlus),
|
||||
IconFolderPlus: markRaw(IconFolderPlus),
|
||||
IconMoreVertical: markRaw(IconMoreVertical),
|
||||
IconEdit: markRaw(IconEdit),
|
||||
IconDownload: markRaw(IconDownload),
|
||||
IconTrash2: markRaw(IconTrash2),
|
||||
|
||||
showChildren: false,
|
||||
dragging: false,
|
||||
selectedFolder: {},
|
||||
prevCursor: "",
|
||||
cursor: "",
|
||||
pageNo: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelected(): boolean {
|
||||
return (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "my-collection" &&
|
||||
this.picked.collectionIndex === this.collectionIndex
|
||||
)
|
||||
},
|
||||
getCollectionIcon() {
|
||||
if (this.isSelected) return IconCheckCircle
|
||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
exportCollection() {
|
||||
const collectionJSON = JSON.stringify(this.collection)
|
||||
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
a.download = `${this.collection.name}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.toast.success(this.t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
},
|
||||
toggleShowChildren() {
|
||||
if (this.$props.saveRequest)
|
||||
this.$emit("select", {
|
||||
picked: {
|
||||
pickedType: "my-collection",
|
||||
collectionIndex: this.collectionIndex,
|
||||
},
|
||||
})
|
||||
|
||||
this.$emit("expand-collection", this.collection.id)
|
||||
this.showChildren = !this.showChildren
|
||||
},
|
||||
removeCollection() {
|
||||
this.$emit("remove-collection", {
|
||||
collectionIndex: this.collectionIndex,
|
||||
collectionID: this.collection.id,
|
||||
})
|
||||
},
|
||||
dropEvent({ dataTransfer }: any) {
|
||||
this.dragging = !this.dragging
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
moveRESTRequest(folderPath, requestIndex, `${this.collectionIndex}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,340 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<component
|
||||
:is="getCollectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ folder.name ? folder.name : folder.title }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="$emit('add-request', { path: folderPath })"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="$emit('add-folder', { folder, path: folderPath })"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction.$el.click()"
|
||||
@keyup.n="folderAction.$el.click()"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.x="exportAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-request', { path: folderPath })
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-folder', { folder, path: folderPath })
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('edit-folder', {
|
||||
folder,
|
||||
folderIndex,
|
||||
collectionIndex,
|
||||
folderPath,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
@click="
|
||||
() => {
|
||||
exportFolder()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeFolder()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<!-- Referring to this component only (this is recursive) -->
|
||||
<Folder
|
||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:collection-index="collectionIndex"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||
:picked="picked"
|
||||
@add-request="$emit('add-request', $event)"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@update-team-collections="$emit('update-team-collections')"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@remove-folder="$emit('remove-folder', $event)"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-name="folder.name"
|
||||
:folder-path="folderPath"
|
||||
:request-index="index"
|
||||
:picked="picked"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import IconFilePlus from "~icons/lucide/file-plus"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { defineComponent, ref } from "vue"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { moveRESTRequest } from "~/newstore/collections"
|
||||
|
||||
export default defineComponent({
|
||||
name: "Folder",
|
||||
props: {
|
||||
folder: { type: Object, default: () => ({}) },
|
||||
folderIndex: { type: Number, default: null },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
folderPath: { type: String, default: null },
|
||||
saveRequest: Boolean,
|
||||
isFiltered: Boolean,
|
||||
collectionsType: { type: Object, default: () => ({}) },
|
||||
picked: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: [
|
||||
"add-request",
|
||||
"add-folder",
|
||||
"edit-folder",
|
||||
"update-team",
|
||||
"remove-folder",
|
||||
"edit-request",
|
||||
"duplicate-request",
|
||||
"select",
|
||||
"remove-request",
|
||||
"update-team-collections",
|
||||
],
|
||||
setup() {
|
||||
const t = useI18n()
|
||||
|
||||
return {
|
||||
// Template refs
|
||||
tippyActions: ref<any | null>(null),
|
||||
options: ref<any | null>(null),
|
||||
requestAction: ref<any | null>(null),
|
||||
folderAction: ref<any | null>(null),
|
||||
edit: ref<any | null>(null),
|
||||
deleteAction: ref<any | null>(null),
|
||||
exportAction: ref<any | null>(null),
|
||||
t,
|
||||
toast: useToast(),
|
||||
colorMode: useColorMode(),
|
||||
IconFilePlus,
|
||||
IconFolderPlus,
|
||||
IconMoreVertical,
|
||||
IconEdit,
|
||||
IconDownload,
|
||||
IconTrash2,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showChildren: false,
|
||||
dragging: false,
|
||||
prevCursor: "",
|
||||
cursor: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelected(): boolean {
|
||||
return (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "my-folder" &&
|
||||
this.picked.folderPath === this.folderPath
|
||||
)
|
||||
},
|
||||
getCollectionIcon() {
|
||||
if (this.isSelected) return IconCheckCircle
|
||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
exportFolder() {
|
||||
const folderJSON = JSON.stringify(this.folder)
|
||||
|
||||
const file = new Blob([folderJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
a.download = `${this.folder.name}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.toast.success(this.t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
},
|
||||
toggleShowChildren() {
|
||||
if (this.$props.saveRequest)
|
||||
this.$emit("select", {
|
||||
picked: {
|
||||
pickedType: "my-folder",
|
||||
collectionIndex: this.collectionIndex,
|
||||
folderName: this.folder.name,
|
||||
folderPath: this.folderPath,
|
||||
},
|
||||
})
|
||||
this.showChildren = !this.showChildren
|
||||
},
|
||||
removeFolder() {
|
||||
this.$emit("remove-folder", {
|
||||
folder: this.folder,
|
||||
folderPath: this.folderPath,
|
||||
})
|
||||
},
|
||||
dropEvent({ dataTransfer }) {
|
||||
this.dragging = !this.dragging
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
moveRESTRequest(folderPath, requestIndex, this.folderPath)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,433 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover.stop
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||
:class="getRequestLabelColor(request.method)"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<component
|
||||
:is="IconCheckCircle"
|
||||
v-if="isSelected"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<span v-else class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isActive"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
||||
:title="`${t('collection.request_in_use')}`"
|
||||
>
|
||||
<span
|
||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconRotateCCW"
|
||||
:title="t('action.restore')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="selectRequest()"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.d="duplicate.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-request', {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
folderPath,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="t('action.duplicate')"
|
||||
:shortcut="['D']"
|
||||
@click="
|
||||
() => {
|
||||
emit('duplicate-request', {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
folderPath,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeRequest()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<HttpReqChangeConfirmModal
|
||||
:show="confirmChange"
|
||||
@hide-modal="confirmChange = false"
|
||||
@save-change="saveRequestChange"
|
||||
@discard-change="discardRequestChange"
|
||||
/>
|
||||
<CollectionsSaveRequest
|
||||
mode="rest"
|
||||
:show="showSaveRequestModal"
|
||||
@hide-modal="showSaveRequestModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import { ref, computed } from "vue"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
safelyExtractRESTRequest,
|
||||
translateToNewRequest,
|
||||
isEqualHoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import {
|
||||
getDefaultRESTRequest,
|
||||
getRESTRequest,
|
||||
restSaveContext$,
|
||||
setRESTRequest,
|
||||
setRESTSaveContext,
|
||||
getRESTSaveContext,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
|
||||
const props = defineProps<{
|
||||
request: HoppRESTRequest
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName: string
|
||||
requestIndex: number
|
||||
saveRequest: boolean
|
||||
collectionsType: object
|
||||
folderPath: string
|
||||
picked?: {
|
||||
pickedType: string
|
||||
collectionIndex: number
|
||||
folderPath: string
|
||||
folderName: string
|
||||
requestIndex: number
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "select",
|
||||
data:
|
||||
| {
|
||||
picked: {
|
||||
pickedType: string
|
||||
collectionIndex: number
|
||||
folderPath: string
|
||||
folderName: string
|
||||
requestIndex: number
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
): void
|
||||
|
||||
(
|
||||
e: "remove-request",
|
||||
data: {
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}
|
||||
): void
|
||||
|
||||
(
|
||||
e: "duplicate-request",
|
||||
data: {
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName: string
|
||||
request: HoppRESTRequest
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}
|
||||
): void
|
||||
|
||||
(
|
||||
e: "edit-request",
|
||||
data: {
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName: string
|
||||
request: HoppRESTRequest
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const dragging = ref(false)
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
post: "text-yellow-500",
|
||||
put: "text-blue-500",
|
||||
delete: "text-red-500",
|
||||
default: "text-gray-500",
|
||||
}
|
||||
const confirmChange = ref(false)
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const options = ref<any | null>(null)
|
||||
const edit = ref<any | null>(null)
|
||||
const duplicate = ref<any | null>(null)
|
||||
const deleteAction = ref<any | null>(null)
|
||||
|
||||
const active = useReadonlyStream(restSaveContext$, null)
|
||||
|
||||
const isSelected = computed(
|
||||
() =>
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-request" &&
|
||||
props.picked.folderPath === props.folderPath &&
|
||||
props.picked.requestIndex === props.requestIndex
|
||||
)
|
||||
|
||||
const isActive = computed(
|
||||
() =>
|
||||
active.value &&
|
||||
active.value.originLocation === "user-collection" &&
|
||||
active.value.folderPath === props.folderPath &&
|
||||
active.value.requestIndex === props.requestIndex
|
||||
)
|
||||
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
dragging.value = !dragging.value
|
||||
dataTransfer.setData("folderPath", props.folderPath)
|
||||
dataTransfer.setData("requestIndex", props.requestIndex.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const removeRequest = () => {
|
||||
emit("remove-request", {
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const getRequestLabelColor = (method: string) =>
|
||||
requestMethodLabels[
|
||||
method.toLowerCase() as keyof typeof requestMethodLabels
|
||||
] || requestMethodLabels.default
|
||||
|
||||
const setRestReq = (request: any) => {
|
||||
setRESTRequest(
|
||||
cloneDeep(
|
||||
safelyExtractRESTRequest(
|
||||
translateToNewRequest(request),
|
||||
getDefaultRESTRequest()
|
||||
)
|
||||
),
|
||||
{
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
req: cloneDeep(request),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
|
||||
const selectRequest = () => {
|
||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
||||
if (props.saveRequest) {
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "my-request",
|
||||
collectionIndex: props.collectionIndex,
|
||||
folderPath: props.folderPath,
|
||||
folderName: props.folderName,
|
||||
requestIndex: props.requestIndex,
|
||||
},
|
||||
})
|
||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
||||
confirmChange.value = false
|
||||
setRestReq(props.request)
|
||||
} else if (!active.value) {
|
||||
// If the current request is the same as the request to be loaded in, there is no data loss
|
||||
const currentReq = getRESTRequest()
|
||||
|
||||
if (isEqualHoppRESTRequest(currentReq, props.request)) {
|
||||
setRestReq(props.request)
|
||||
} else {
|
||||
confirmChange.value = true
|
||||
}
|
||||
} else {
|
||||
const currentReqWithNoChange = active.value.req
|
||||
const currentFullReq = getRESTRequest()
|
||||
|
||||
// Check if whether user clicked the same request or not
|
||||
if (!isActive.value && currentReqWithNoChange !== undefined) {
|
||||
// Check if there is any changes done on the current request
|
||||
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
|
||||
setRestReq(props.request)
|
||||
} else {
|
||||
confirmChange.value = true
|
||||
}
|
||||
} else {
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Save current request to the collection */
|
||||
const saveRequestChange = () => {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
saveCurrentRequest(saveCtx)
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
/** Discard changes and change the current request and context */
|
||||
const discardRequestChange = () => {
|
||||
setRestReq(props.request)
|
||||
if (!isActive.value) {
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
req: cloneDeep(props.request),
|
||||
})
|
||||
}
|
||||
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
||||
if (!saveCtx) {
|
||||
showSaveRequestModal.value = true
|
||||
return
|
||||
}
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
try {
|
||||
editRESTRequest(
|
||||
saveCtx.folderPath,
|
||||
saveCtx.requestIndex,
|
||||
getRESTRequest()
|
||||
)
|
||||
setRestReq(props.request)
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
setRESTSaveContext(null)
|
||||
saveCurrentRequest(saveCtx)
|
||||
}
|
||||
} else if (saveCtx.originLocation === "team-collection") {
|
||||
const req = getRESTRequest()
|
||||
try {
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
title: req.name,
|
||||
request: JSON.stringify(req),
|
||||
},
|
||||
})().then((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
} else {
|
||||
toast.success(`${t("request.saved")}`)
|
||||
}
|
||||
})
|
||||
setRestReq(props.request)
|
||||
} catch (error) {
|
||||
showSaveRequestModal.value = true
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,406 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<component
|
||||
:is="getCollectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collection.title }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="
|
||||
$emit('add-request', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="
|
||||
$emit('add-folder', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction.$el.click()"
|
||||
@keyup.n="folderAction.$el.click()"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.x="exportAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-request', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-folder', {
|
||||
folder: collection,
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="exportCollection"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeCollection()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsTeamsFolder
|
||||
v-for="(folder, index) in collection.children"
|
||||
:key="`folder-${index}`"
|
||||
:folder="folder"
|
||||
:folder-index="index"
|
||||
:folder-path="`${collectionIndex}/${index}`"
|
||||
:collection-index="collectionIndex"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:is-filtered="isFiltered"
|
||||
:picked="picked"
|
||||
:loading-collection-i-ds="loadingCollectionIDs"
|
||||
@add-request="$emit('add-request', $event)"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@expand-collection="expandCollection"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@remove-folder="$emit('remove-folder', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request.request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:request-index="request.id"
|
||||
:save-request="saveRequest"
|
||||
:collection-i-d="collection.id"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
@edit-request="editRequest($event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="loadingCollectionIDs.includes(collection.id)"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(collection.children == undefined ||
|
||||
collection.children.length === 0) &&
|
||||
(collection.requests == undefined ||
|
||||
collection.requests.length === 0)
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconFilePlus from "~icons/lucide/file-plus"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { defineComponent, ref } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
getCompleteCollectionTree,
|
||||
teamCollToHoppRESTColl,
|
||||
} from "~/helpers/backend/helpers"
|
||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collectionIndex: { type: Number, default: null },
|
||||
collection: { type: Object, default: () => ({}) },
|
||||
isFiltered: Boolean,
|
||||
saveRequest: Boolean,
|
||||
collectionsType: { type: Object, default: () => ({}) },
|
||||
picked: { type: Object, default: () => ({}) },
|
||||
loadingCollectionIDs: { type: Array, default: () => [] },
|
||||
},
|
||||
emits: [
|
||||
"edit-collection",
|
||||
"add-request",
|
||||
"add-folder",
|
||||
"edit-folder",
|
||||
"edit-request",
|
||||
"remove-folder",
|
||||
"select",
|
||||
"remove-request",
|
||||
"duplicate-request",
|
||||
"expand-collection",
|
||||
"remove-collection",
|
||||
],
|
||||
setup() {
|
||||
const t = useI18n()
|
||||
|
||||
return {
|
||||
// Template refs
|
||||
tippyActions: ref<any | null>(null),
|
||||
options: ref<any | null>(null),
|
||||
requestAction: ref<any | null>(null),
|
||||
folderAction: ref<any | null>(null),
|
||||
edit: ref<any | null>(null),
|
||||
deleteAction: ref<any | null>(null),
|
||||
exportAction: ref<any | null>(null),
|
||||
exportLoading: ref<boolean>(false),
|
||||
t,
|
||||
toast: useToast(),
|
||||
colorMode: useColorMode(),
|
||||
|
||||
IconCheckCircle,
|
||||
IconCircle,
|
||||
IconFilePlus,
|
||||
IconFolderPlus,
|
||||
IconEdit,
|
||||
IconDownload,
|
||||
IconTrash2,
|
||||
IconMoreVertical,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showChildren: false,
|
||||
dragging: false,
|
||||
selectedFolder: {},
|
||||
prevCursor: "",
|
||||
cursor: "",
|
||||
pageNo: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelected(): boolean {
|
||||
return (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "teams-collection" &&
|
||||
this.picked.collectionID === this.collection.id
|
||||
)
|
||||
},
|
||||
getCollectionIcon() {
|
||||
if (this.isSelected) return IconCheckCircle
|
||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async exportCollection() {
|
||||
this.exportLoading = true
|
||||
|
||||
const result = await getCompleteCollectionTree(this.collection.id)()
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong").toString())
|
||||
console.log(result.left)
|
||||
this.exportLoading = false
|
||||
this.options.tippy().hide()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const hoppColl = teamCollToHoppRESTColl(result.right)
|
||||
|
||||
const collectionJSON = JSON.stringify(hoppColl)
|
||||
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
a.download = `${hoppColl.name}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.toast.success(this.t("state.download_started").toString())
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
this.exportLoading = false
|
||||
|
||||
this.options.tippy().hide()
|
||||
},
|
||||
editRequest(event: any) {
|
||||
this.$emit("edit-request", event)
|
||||
if (this.$props.saveRequest)
|
||||
this.$emit("select", {
|
||||
picked: {
|
||||
pickedType: "teams-collection",
|
||||
collectionID: this.collection.id,
|
||||
},
|
||||
})
|
||||
},
|
||||
toggleShowChildren() {
|
||||
if (this.$props.saveRequest)
|
||||
this.$emit("select", {
|
||||
picked: {
|
||||
pickedType: "teams-collection",
|
||||
collectionID: this.collection.id,
|
||||
},
|
||||
})
|
||||
|
||||
this.$emit("expand-collection", this.collection.id)
|
||||
this.showChildren = !this.showChildren
|
||||
},
|
||||
removeCollection() {
|
||||
this.$emit("remove-collection", {
|
||||
collectionIndex: this.collectionIndex,
|
||||
collectionID: this.collection.id,
|
||||
})
|
||||
},
|
||||
expandCollection(collectionID: string) {
|
||||
this.$emit("expand-collection", collectionID)
|
||||
},
|
||||
async dropEvent({ dataTransfer }: any) {
|
||||
this.dragging = !this.dragging
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const moveRequestResult = await moveRESTTeamRequest(
|
||||
requestIndex,
|
||||
this.collection.id
|
||||
)()
|
||||
if (E.isLeft(moveRequestResult))
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<component
|
||||
:is="getCollectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ folder.name ? folder.name : folder.title }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="$emit('add-request', { folder, path: folderPath })"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="$emit('add-folder', { folder, path: folderPath })"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction.$el.click()"
|
||||
@keyup.n="folderAction.$el.click()"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.x="exportAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-request', { folder, path: folderPath })
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add-folder', { folder, path: folderPath })
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
$emit('edit-folder', {
|
||||
folder,
|
||||
folderIndex,
|
||||
collectionIndex,
|
||||
folderPath: '',
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="exportFolder"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeFolder()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<!-- Referring to this component only (this is recursive) -->
|
||||
<Folder
|
||||
v-for="(subFolder, subFolderIndex) in folder.children"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:collection-index="collectionIndex"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||
:picked="picked"
|
||||
:loading-collection-i-ds="loadingCollectionIDs"
|
||||
@add-request="$emit('add-request', $event)"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@update-team-collections="$emit('update-team-collections')"
|
||||
@select="$emit('select', $event)"
|
||||
@expand-collection="expandCollection"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@remove-folder="$emit('remove-folder', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request.request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-name="folder.name"
|
||||
:request-index="request.id"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
:collection-i-d="folder.id"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="loadingCollectionIDs.includes(folder.id)"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(folder.children == undefined || folder.children.length === 0) &&
|
||||
(folder.requests == undefined || folder.requests.length === 0)
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconFilePlus from "~icons/lucide/file-plus"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { defineComponent, ref } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
getCompleteCollectionTree,
|
||||
teamCollToHoppRESTColl,
|
||||
} from "~/helpers/backend/helpers"
|
||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
|
||||
export default defineComponent({
|
||||
name: "Folder",
|
||||
props: {
|
||||
folder: { type: Object, default: () => ({}) },
|
||||
folderIndex: { type: Number, default: null },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
folderPath: { type: String, default: null },
|
||||
saveRequest: Boolean,
|
||||
isFiltered: Boolean,
|
||||
collectionsType: { type: Object, default: () => ({}) },
|
||||
picked: { type: Object, default: () => ({}) },
|
||||
loadingCollectionIDs: { type: Array, default: () => [] },
|
||||
},
|
||||
emits: [
|
||||
"add-request",
|
||||
"add-folder",
|
||||
"edit-folder",
|
||||
"update-team-collections",
|
||||
"edit-request",
|
||||
"remove-request",
|
||||
"duplicate-request",
|
||||
"select",
|
||||
"remove-folder",
|
||||
"expand-collection",
|
||||
],
|
||||
setup() {
|
||||
return {
|
||||
// Template refs
|
||||
tippyActions: ref<any | null>(null),
|
||||
options: ref<any | null>(null),
|
||||
requestAction: ref<any | null>(null),
|
||||
folderAction: ref<any | null>(null),
|
||||
edit: ref<any | null>(null),
|
||||
deleteAction: ref<any | null>(null),
|
||||
exportAction: ref<any | null>(null),
|
||||
exportLoading: ref<boolean>(false),
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
colorMode: useColorMode(),
|
||||
IconFilePlus,
|
||||
IconFolderPlus,
|
||||
IconCheckCircle,
|
||||
IconFolder,
|
||||
IconFolderOpen,
|
||||
IconMoreVertical,
|
||||
IconEdit,
|
||||
IconDownload,
|
||||
IconTrash2,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showChildren: false,
|
||||
dragging: false,
|
||||
prevCursor: "",
|
||||
cursor: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelected(): boolean {
|
||||
return (
|
||||
this.picked &&
|
||||
this.picked.pickedType === "teams-folder" &&
|
||||
this.picked.folderID === this.folder.id
|
||||
)
|
||||
},
|
||||
getCollectionIcon() {
|
||||
if (this.isSelected) return IconCheckCircle
|
||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async exportFolder() {
|
||||
this.exportLoading = true
|
||||
|
||||
const result = await getCompleteCollectionTree(this.folder.id)()
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
this.toast.error(this.t("error.something_went_wrong").toString())
|
||||
console.log(result.left)
|
||||
this.exportLoading = false
|
||||
this.options.tippy().hide()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const hoppColl = teamCollToHoppRESTColl(result.right)
|
||||
|
||||
const collectionJSON = JSON.stringify(hoppColl)
|
||||
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
a.download = `${hoppColl.name}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.toast.success(this.t("state.download_started").toString())
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
this.exportLoading = false
|
||||
|
||||
this.options.tippy().hide()
|
||||
},
|
||||
toggleShowChildren() {
|
||||
if (this.$props.saveRequest)
|
||||
this.$emit("select", {
|
||||
picked: {
|
||||
pickedType: "teams-folder",
|
||||
folderID: this.folder.id,
|
||||
},
|
||||
})
|
||||
|
||||
this.$emit("expand-collection", this.$props.folder.id)
|
||||
this.showChildren = !this.showChildren
|
||||
},
|
||||
removeFolder() {
|
||||
this.$emit("remove-folder", {
|
||||
collectionsType: this.collectionsType,
|
||||
folder: this.folder,
|
||||
})
|
||||
},
|
||||
expandCollection(collectionID: number) {
|
||||
this.$emit("expand-collection", collectionID)
|
||||
},
|
||||
async dropEvent({ dataTransfer }: any) {
|
||||
this.dragging = !this.dragging
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const moveRequestResult = await moveRESTTeamRequest(
|
||||
requestIndex,
|
||||
this.folder.id
|
||||
)()
|
||||
if (E.isLeft(moveRequestResult))
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,405 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover.stop
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||
:class="getRequestLabelColor(request.method)"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<component
|
||||
:is="IconCheckCircle"
|
||||
v-if="isSelected"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<span v-else class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isActive"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
||||
:title="`${t('collection.request_in_use')}`"
|
||||
>
|
||||
<span
|
||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconRotateCCW"
|
||||
:title="t('action.restore')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="selectRequest()"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.e="edit.$el.click()"
|
||||
@keyup.d="duplicate.$el.click()"
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-request', {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="t('action.duplicate')"
|
||||
:shortcut="['D']"
|
||||
@click="
|
||||
() => {
|
||||
emit('duplicate-request', {
|
||||
request,
|
||||
requestIndex,
|
||||
collectionID,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
removeRequest()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<HttpReqChangeConfirmModal
|
||||
:show="confirmChange"
|
||||
@hide-modal="confirmChange = false"
|
||||
@save-change="saveRequestChange"
|
||||
@discard-change="discardRequestChange"
|
||||
/>
|
||||
<CollectionsSaveRequest
|
||||
mode="rest"
|
||||
:show="showSaveRequestModal"
|
||||
@hide-modal="showSaveRequestModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { ref, computed } from "vue"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
isEqualHoppRESTRequest,
|
||||
safelyExtractRESTRequest,
|
||||
translateToNewRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import {
|
||||
getDefaultRESTRequest,
|
||||
restSaveContext$,
|
||||
setRESTRequest,
|
||||
setRESTSaveContext,
|
||||
getRESTSaveContext,
|
||||
getRESTRequest,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { Team, UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
|
||||
const props = defineProps<{
|
||||
request: HoppRESTRequest
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName?: string
|
||||
requestIndex: string
|
||||
saveRequest: boolean
|
||||
collectionsType: {
|
||||
type: "my-collections" | "team-collections"
|
||||
selectedTeam: Team | undefined
|
||||
}
|
||||
collectionID: string
|
||||
picked?: {
|
||||
pickedType: string
|
||||
requestID: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "select",
|
||||
data:
|
||||
| {
|
||||
picked: {
|
||||
pickedType: string
|
||||
requestID: string
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
): void
|
||||
|
||||
(
|
||||
e: "remove-request",
|
||||
data: {
|
||||
folderPath: string | undefined
|
||||
requestIndex: string
|
||||
}
|
||||
): void
|
||||
|
||||
(
|
||||
e: "edit-request",
|
||||
data: {
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName: string | undefined
|
||||
requestIndex: string
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
|
||||
(
|
||||
e: "duplicate-request",
|
||||
data: {
|
||||
collectionID: number | string
|
||||
requestIndex: string
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const dragging = ref(false)
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
post: "text-yellow-500",
|
||||
put: "text-blue-500",
|
||||
delete: "text-red-500",
|
||||
default: "text-gray-500",
|
||||
}
|
||||
const confirmChange = ref(false)
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const options = ref<any | null>(null)
|
||||
const edit = ref<any | null>(null)
|
||||
const duplicate = ref<any | null>(null)
|
||||
const deleteAction = ref<any | null>(null)
|
||||
|
||||
const active = useReadonlyStream(restSaveContext$, null)
|
||||
|
||||
const isSelected = computed(
|
||||
() =>
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-request" &&
|
||||
props.picked.requestID === props.requestIndex
|
||||
)
|
||||
|
||||
const isActive = computed(
|
||||
() =>
|
||||
active.value &&
|
||||
active.value.originLocation === "team-collection" &&
|
||||
active.value.requestID === props.requestIndex
|
||||
)
|
||||
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
dragging.value = !dragging.value
|
||||
dataTransfer.setData("requestIndex", props.requestIndex)
|
||||
}
|
||||
}
|
||||
|
||||
const removeRequest = () => {
|
||||
emit("remove-request", {
|
||||
folderPath: props.folderName,
|
||||
requestIndex: props.requestIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const getRequestLabelColor = (method: string): string => {
|
||||
return (
|
||||
(requestMethodLabels as any)[method.toLowerCase()] ||
|
||||
requestMethodLabels.default
|
||||
)
|
||||
}
|
||||
|
||||
const setRestReq = (request: HoppRESTRequest) => {
|
||||
setRESTRequest(
|
||||
safelyExtractRESTRequest(
|
||||
translateToNewRequest(request),
|
||||
getDefaultRESTRequest()
|
||||
),
|
||||
{
|
||||
originLocation: "team-collection",
|
||||
requestID: props.requestIndex,
|
||||
req: request,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const selectRequest = () => {
|
||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
||||
if (props.saveRequest) {
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "teams-request",
|
||||
requestID: props.requestIndex,
|
||||
},
|
||||
})
|
||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
||||
confirmChange.value = false
|
||||
setRestReq(props.request)
|
||||
} else if (!active.value) {
|
||||
confirmChange.value = true
|
||||
} else {
|
||||
const currentReqWithNoChange = active.value.req
|
||||
const currentFullReq = getRESTRequest()
|
||||
|
||||
// Check if whether user clicked the same request or not
|
||||
if (!isActive.value && currentReqWithNoChange) {
|
||||
// Check if there is any changes done on the current request
|
||||
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
|
||||
setRestReq(props.request)
|
||||
} else {
|
||||
confirmChange.value = true
|
||||
}
|
||||
} else {
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Save current request to the collection */
|
||||
const saveRequestChange = () => {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
saveCurrentRequest(saveCtx)
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
/** Discard changes and change the current request and context */
|
||||
const discardRequestChange = () => {
|
||||
setRestReq(props.request)
|
||||
if (!isActive.value) {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: props.requestIndex,
|
||||
req: props.request,
|
||||
})
|
||||
}
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
||||
if (!saveCtx) {
|
||||
showSaveRequestModal.value = true
|
||||
return
|
||||
}
|
||||
if (saveCtx.originLocation === "team-collection") {
|
||||
const req = getRESTRequest()
|
||||
try {
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
title: req.name,
|
||||
request: JSON.stringify(req),
|
||||
},
|
||||
})().then((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
} else {
|
||||
toast.success(`${t("request.saved")}`)
|
||||
}
|
||||
})
|
||||
setRestReq(props.request)
|
||||
} catch (error) {
|
||||
showSaveRequestModal.value = true
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(error)
|
||||
}
|
||||
} else if (saveCtx.originLocation === "user-collection") {
|
||||
try {
|
||||
editRESTRequest(
|
||||
saveCtx.folderPath,
|
||||
saveCtx.requestIndex,
|
||||
getRESTRequest()
|
||||
)
|
||||
setRestReq(props.request)
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
setRESTSaveContext(null)
|
||||
saveCurrentRequest(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,197 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="sticky top-0 z-10 flex flex-col rounded-t bg-primary">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-if="selectedEnvironmentIndex !== -1"
|
||||
:label="environments[selectedEnvironmentIndex].name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="`${t('environment.select')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="selectedEnvironmentIndex === -1 ? IconDone : null"
|
||||
:active-info-icon="selectedEnvironmentIndex === -1"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = -1
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<hr v-if="environments.length > 0" />
|
||||
<SmartItem
|
||||
v-for="(gen, index) in environments"
|
||||
:key="`gen-${index}`"
|
||||
:label="gen.name"
|
||||
:info-icon="index === selectedEnvironmentIndex ? IconDone : null"
|
||||
:active-info-icon="index === selectedEnvironmentIndex"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = index
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<div class="flex justify-between flex-1 border-b border-dividerLight">
|
||||
<ButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="`${t('action.new')}`"
|
||||
class="!rounded-none"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
:title="t('modal.import_export')"
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<EnvironmentsEnvironment
|
||||
environment-index="Global"
|
||||
:environment="globalEnvironment"
|
||||
class="border-b border-dashed border-dividerLight"
|
||||
@edit-environment="editEnvironment('Global')"
|
||||
/>
|
||||
<EnvironmentsEnvironment
|
||||
v-for="(environment, index) in environments"
|
||||
:key="`environment-${index}`"
|
||||
:environment-index="index"
|
||||
:environment="environment"
|
||||
@edit-environment="editEnvironment(index)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="environments.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsDetails
|
||||
:show="showModalDetails"
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
:show="showModalImportExport"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import { computed, ref } from "vue"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import {
|
||||
environments$,
|
||||
setCurrentEnvironment,
|
||||
selectedEnvIndex$,
|
||||
globalEnv$,
|
||||
} from "~/newstore/environments"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
const globalEnvironment = computed(() => ({
|
||||
name: "Global",
|
||||
variables: globalEnv.value,
|
||||
}))
|
||||
|
||||
const environments = useReadonlyStream(environments$, [])
|
||||
|
||||
const selectedEnvironmentIndex = useStream(
|
||||
selectedEnvIndex$,
|
||||
-1,
|
||||
setCurrentEnvironment
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const showModalImportExport = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<number | "Global" | null>(null)
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
showModalDetails.value = shouldDisplay
|
||||
}
|
||||
const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
action.value = "edit"
|
||||
showModalDetails.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
const displayModalImportExport = (shouldDisplay: boolean) => {
|
||||
showModalImportExport.value = shouldDisplay
|
||||
}
|
||||
const editEnvironment = (environmentIndex: number | "Global") => {
|
||||
editingEnvironmentIndex.value = environmentIndex
|
||||
action.value = "edit"
|
||||
displayModalEdit(true)
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -1,302 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("authorization.type") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary class="pr-8 ml-2 rounded-none" :label="authName" />
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Basic Auth'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'basic'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="Bearer Token"
|
||||
:icon="authName === 'Bearer' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Bearer'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'bearer'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="OAuth 2.0"
|
||||
:icon="authName === 'OAuth 2.0' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'oauth-2'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="API key"
|
||||
:icon="authName === 'API key' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'API key'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'api-key'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<!-- <SmartCheckbox
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</SmartCheckbox>-->
|
||||
<SmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>{{ t("state.enabled") }}</SmartCheckbox
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'none'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.authorization") }}</span>
|
||||
<ButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="authType === 'basic'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicUsername"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicPassword"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
</div>
|
||||
<div v-if="authType === 'api-key'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiKey" placeholder="Key" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiValue" placeholder="Value" />
|
||||
</div>
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
<label class="ml-4 text-secondaryLight">
|
||||
{{ t("authorization.pass_key_by") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
:label="addTo || t('state.none')"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||
:active="addTo === 'Headers'"
|
||||
:label="'Headers'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Headers'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
:icon="
|
||||
addTo === 'Query params' ? IconCircleDot : IconCircle
|
||||
"
|
||||
:active="addTo === 'Query params'"
|
||||
:label="'Query params'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Query params'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
|
||||
>
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="t('authorization.learn')"
|
||||
:icon="IconExternalLink"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import { computed, ref, Ref } from "vue"
|
||||
import {
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTAuthAPIKey,
|
||||
} from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
const authType = pluckRef(auth, "authType")
|
||||
const authName = computed(() => {
|
||||
if (authType.value === "basic") return "Basic Auth"
|
||||
else if (authType.value === "bearer") return "Bearer"
|
||||
else if (authType.value === "oauth-2") return "OAuth 2.0"
|
||||
else if (authType.value === "api-key") return "API key"
|
||||
else return "None"
|
||||
})
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
const basicUsername = pluckRef(auth as Ref<HoppRESTAuthBasic>, "username")
|
||||
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
|
||||
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
|
||||
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
|
||||
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
|
||||
if (typeof addTo.value === "undefined") {
|
||||
addTo.value = "Headers"
|
||||
apiKey.value = ""
|
||||
apiValue.value = ""
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const authTippyActions = ref<any | null>(null)
|
||||
</script>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('import.curl')}`"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="px-2 h-46">
|
||||
<div
|
||||
ref="curlEditor"
|
||||
class="h-full border rounded border-dividerLight"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
ref="importButton"
|
||||
:label="`${t('import.title')}`"
|
||||
outline
|
||||
@click="handleImport"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<span class="flex">
|
||||
<ButtonSecondary
|
||||
:icon="pasteIcon"
|
||||
:label="`${t('action.paste')}`"
|
||||
filled
|
||||
outline
|
||||
@click="handlePaste"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
|
||||
|
||||
import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const curl = ref("")
|
||||
|
||||
const curlEditor = ref<any | null>(null)
|
||||
|
||||
const props = defineProps<{ show: boolean; text: string }>()
|
||||
|
||||
useCodemirror(curlEditor, curl, {
|
||||
extendedEditorConfig: {
|
||||
mode: "application/x-sh",
|
||||
placeholder: `${t("request.enter_curl")}`,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
if (props.show) {
|
||||
curl.value = props.text.toString()
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const handleImport = () => {
|
||||
const text = curl.value
|
||||
try {
|
||||
const req = parseCurlToHoppRESTReq(text)
|
||||
|
||||
setRESTRequest(req)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`${t("error.curl_invalid_format")}`)
|
||||
}
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
|
||||
IconClipboard,
|
||||
1000
|
||||
)
|
||||
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
curl.value = text
|
||||
pasteIcon.value = IconCheck
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to copy: ", e)
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="oidcDiscoveryURL"
|
||||
placeholder="OpenID Connect Discovery URL"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="clientSecret" placeholder="Client Secret" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="scope" placeholder="Scope" />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ButtonSecondary
|
||||
filled
|
||||
:label="`${t('authorization.generate_token')}`"
|
||||
@click="handleAccessTokenRequest()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref, defineComponent } from "vue"
|
||||
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
import { tokenRequest } from "~/helpers/oauth"
|
||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
|
||||
const oidcDiscoveryURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"oidcDiscoveryURL"
|
||||
)
|
||||
|
||||
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
|
||||
|
||||
const accessTokenURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"accessTokenURL"
|
||||
)
|
||||
|
||||
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
|
||||
|
||||
const clientSecret = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"clientSecret"
|
||||
)
|
||||
|
||||
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
|
||||
|
||||
const handleAccessTokenRequest = async () => {
|
||||
if (
|
||||
oidcDiscoveryURL.value === "" &&
|
||||
(authURL.value === "" || accessTokenURL.value === "")
|
||||
) {
|
||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||
return
|
||||
}
|
||||
const envs = getCombinedEnvVariables()
|
||||
const envVars = [...envs.selected, ...envs.global]
|
||||
|
||||
try {
|
||||
const tokenReqParams = {
|
||||
grantType: "code",
|
||||
oidcDiscoveryUrl: parseTemplateString(
|
||||
oidcDiscoveryURL.value,
|
||||
envVars
|
||||
),
|
||||
authUrl: parseTemplateString(authURL.value, envVars),
|
||||
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
|
||||
clientId: parseTemplateString(clientID.value, envVars),
|
||||
clientSecret: parseTemplateString(clientSecret.value, envVars),
|
||||
scope: parseTemplateString(scope.value, envVars),
|
||||
}
|
||||
await tokenRequest(tokenReqParams)
|
||||
} catch (e) {
|
||||
toast.error(`${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oidcDiscoveryURL,
|
||||
authURL,
|
||||
accessTokenURL,
|
||||
clientID,
|
||||
clientSecret,
|
||||
scope,
|
||||
handleAccessTokenRequest,
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<SmartTabs
|
||||
v-model="selectedRealtimeTab"
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
:id="'params'"
|
||||
:label="`${t('tab.parameters')}`"
|
||||
:info="`${newActiveParamsCount$}`"
|
||||
>
|
||||
<HttpParameters />
|
||||
</SmartTab>
|
||||
<SmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
||||
<HttpBody @change-tab="changeTab" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="`${newActiveHeadersCount$}`"
|
||||
>
|
||||
<HttpHeaders @change-tab="changeTab" />
|
||||
</SmartTab>
|
||||
<SmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<HttpAuthorization />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
:id="'preRequestScript'"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
preRequestScript && preRequestScript.length > 0 ? true : false
|
||||
"
|
||||
>
|
||||
<HttpPreRequestScript />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
:id="'tests'"
|
||||
:label="`${t('tab.tests')}`"
|
||||
:indicator="testScript && testScript.length > 0 ? true : false"
|
||||
>
|
||||
<HttpTests />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { map } from "rxjs/operators"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import {
|
||||
restActiveHeadersCount$,
|
||||
restActiveParamsCount$,
|
||||
usePreRequestScript,
|
||||
useTestScript,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
export type RequestOptionTabs =
|
||||
| "params"
|
||||
| "bodyParams"
|
||||
| "headers"
|
||||
| "authorization"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
const changeTab = (e: RequestOptionTabs) => {
|
||||
selectedRealtimeTab.value = e
|
||||
}
|
||||
|
||||
const newActiveParamsCount$ = useReadonlyStream(
|
||||
restActiveParamsCount$.pipe(
|
||||
map((e) => {
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
),
|
||||
null
|
||||
)
|
||||
|
||||
const newActiveHeadersCount$ = useReadonlyStream(
|
||||
restActiveHeadersCount$.pipe(
|
||||
map((e) => {
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
),
|
||||
null
|
||||
)
|
||||
|
||||
const preRequestScript = usePreRequestScript()
|
||||
|
||||
const testScript = useTestScript()
|
||||
</script>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<HttpResponseMeta :response="response" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
:response="response"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch } from "vue"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { restResponse$ } from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const response = useReadonlyStream(restResponse$, null)
|
||||
|
||||
const hasResponse = computed(
|
||||
() =>
|
||||
response.value?.type === "success" || response.value?.type === "fail"
|
||||
)
|
||||
|
||||
const loading = computed(
|
||||
() => response.value === null || response.value.type === "loading"
|
||||
)
|
||||
|
||||
watch(response, () => {
|
||||
if (response.value?.type === "loading") startPageProgress()
|
||||
else completePageProgress()
|
||||
})
|
||||
|
||||
return {
|
||||
hasResponse,
|
||||
response,
|
||||
loading,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<SmartTabs
|
||||
v-if="response"
|
||||
v-model="selectedLensTab"
|
||||
styles="sticky z-10 bg-primary top-lowerPrimaryStickyFold"
|
||||
>
|
||||
<SmartTab
|
||||
v-for="(lens, index) in validLenses"
|
||||
:id="lens.renderer"
|
||||
:key="`lens-${index}`"
|
||||
:label="t(lens.lensName)"
|
||||
class="flex flex-col flex-1 w-full h-full"
|
||||
>
|
||||
<component :is="lens.renderer" :response="response" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="headerLength"
|
||||
id="headers"
|
||||
:label="t('response.headers')"
|
||||
:info="`${headerLength}`"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<LensesHeadersRenderer :headers="response.headers" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
id="results"
|
||||
:label="t('test.results')"
|
||||
:indicator="
|
||||
testResults &&
|
||||
(testResults.expectResults.length ||
|
||||
testResults.tests.length ||
|
||||
testResults.envDiff.selected.additions.length ||
|
||||
testResults.envDiff.selected.updations.length ||
|
||||
testResults.envDiff.global.updations.length)
|
||||
? true
|
||||
: false
|
||||
"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<HttpTestResult />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { getSuitableLenses, getLensRenderers } from "~/helpers/lenses/lenses"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { restTestResults$ } from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// Lens Renderers
|
||||
...getLensRenderers(),
|
||||
},
|
||||
props: {
|
||||
response: { type: Object, default: () => ({}) },
|
||||
},
|
||||
setup() {
|
||||
const testResults = useReadonlyStream(restTestResults$, null)
|
||||
|
||||
return {
|
||||
testResults,
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLensTab: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerLength() {
|
||||
if (!this.response || !this.response.headers) return 0
|
||||
|
||||
return Object.keys(this.response.headers).length
|
||||
},
|
||||
validLenses() {
|
||||
if (!this.response) return []
|
||||
|
||||
return getSuitableLenses(this.response)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
validLenses: {
|
||||
handler(newValue) {
|
||||
if (newValue.length === 0) return
|
||||
this.selectedLensTab = newValue[0].renderer
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col overflow-hidden space-y-2"
|
||||
:class="expand ? 'h-full' : 'max-h-32'"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
<div class="sticky inset-x-0 bottom-0 flex items-center justify-center">
|
||||
<ButtonSecondary
|
||||
:icon="expand ? IconChevronUp : IconChevronDown"
|
||||
:label="expand ? t('action.less') : t('action.more')"
|
||||
filled
|
||||
rounded
|
||||
@click="expand = !expand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconChevronUp from "~icons/lucide/chevron-up"
|
||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const expand = ref(false)
|
||||
</script>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<SmartLink
|
||||
:to="to"
|
||||
:exact="exact"
|
||||
:blank="blank"
|
||||
class="inline-flex items-center flex-shrink-0 px-4 py-2 rounded transition hover:bg-primaryDark hover:text-secondaryDark focus:outline-none focus-visible:bg-primaryDark focus-visible:text-secondaryDark"
|
||||
:class="[
|
||||
{ 'opacity-75 cursor-not-allowed': disabled },
|
||||
{ 'pointer-events-none': loading },
|
||||
{ 'flex-1': label },
|
||||
{ 'flex-row-reverse justify-end': reverse },
|
||||
{
|
||||
'border border-divider hover:border-dividerDark focus-visible:border-dividerDark':
|
||||
outline,
|
||||
},
|
||||
]"
|
||||
:disabled="disabled"
|
||||
:tabindex="loading ? '-1' : '0'"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
v-if="!loading"
|
||||
class="inline-flex items-center"
|
||||
:class="{ 'self-start': !!infoIcon }"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="opacity-75 svg-icons"
|
||||
:class="[
|
||||
label ? (reverse ? 'ml-4' : 'mr-4') : '',
|
||||
{ 'text-accent': active },
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
<SmartSpinner v-else class="mr-4 text-secondaryDark" />
|
||||
<div
|
||||
class="inline-flex items-start flex-1 truncate"
|
||||
:class="{ 'flex-col': description }"
|
||||
>
|
||||
<div class="font-semibold truncate">
|
||||
{{ label }}
|
||||
</div>
|
||||
<p v-if="description" class="my-2 text-left text-secondaryLight">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<component
|
||||
:is="infoIcon"
|
||||
v-if="infoIcon"
|
||||
class="items-center self-center ml-4 svg-icons"
|
||||
:class="{ 'text-accent': activeInfoIcon }"
|
||||
/>
|
||||
<div v-if="shortcut.length" class="ml-4 <sm:hidden font-medium">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="-mr-2 shortcut-key"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</SmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
blank: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
icon: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
svg: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shortcut: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
activeInfoIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
infoIcon: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<icon-lucide-loader class="animate-spin svg-icons" />
|
||||
</template>
|
||||
@@ -1,80 +0,0 @@
|
||||
/* An `action` is a unique verb that is associated with certain thing that can be done on Hoppscotch.
|
||||
* For example, sending a request.
|
||||
*/
|
||||
|
||||
import { onBeforeUnmount, onMounted } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
|
||||
export type HoppAction =
|
||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||
| "request.reset" // Clear request data
|
||||
| "request.copy-link" // Copy Request Link
|
||||
| "request.save" // Save to Collections
|
||||
| "request.save-as" // Save As
|
||||
| "request.method.next" // Select Next Method
|
||||
| "request.method.prev" // Select Previous Method
|
||||
| "request.method.get" // Select GET Method
|
||||
| "request.method.head" // Select HEAD Method
|
||||
| "request.method.post" // Select POST Method
|
||||
| "request.method.put" // Select PUT Method
|
||||
| "request.method.delete" // Select DELETE Method
|
||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||
| "modals.search.toggle" // Shows the search modal
|
||||
| "modals.support.toggle" // Shows the support modal
|
||||
| "modals.share.toggle" // Shows the share modal
|
||||
| "navigation.jump.rest" // Jump to REST page
|
||||
| "navigation.jump.graphql" // Jump to GraphQL page
|
||||
| "navigation.jump.realtime" // Jump to realtime page
|
||||
| "navigation.jump.documentation" // Jump to documentation page
|
||||
| "navigation.jump.settings" // Jump to settings page
|
||||
| "navigation.jump.profile" // Jump to profile page
|
||||
| "settings.theme.system" // Use system theme
|
||||
| "settings.theme.light" // Use light theme
|
||||
| "settings.theme.dark" // Use dark theme
|
||||
| "settings.theme.black" // Use black theme
|
||||
| "response.preview.toggle" // Toggle response preview
|
||||
| "response.file.download" // Download response as file
|
||||
| "response.copy" // Copy response to clipboard
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in HoppAction]?: Array<() => void>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = {}
|
||||
|
||||
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||
|
||||
export function bindAction(action: HoppAction, handler: () => void) {
|
||||
if (boundActions[action]) {
|
||||
boundActions[action]?.push(handler)
|
||||
} else {
|
||||
boundActions[action] = [handler]
|
||||
}
|
||||
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function invokeAction(action: HoppAction) {
|
||||
boundActions[action]?.forEach((handler) => handler())
|
||||
}
|
||||
|
||||
export function unbindAction(action: HoppAction, handler: () => void) {
|
||||
boundActions[action] = boundActions[action]?.filter((x) => x !== handler)
|
||||
|
||||
if (boundActions[action]?.length === 0) {
|
||||
delete boundActions[action]
|
||||
}
|
||||
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function defineActionHandler(action: HoppAction, handler: () => void) {
|
||||
onMounted(() => {
|
||||
bindAction(action, handler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindAction(action, handler)
|
||||
})
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation MoveRESTTeamRequest($requestID: ID!, $collectionID: ID!) {
|
||||
moveRequest(requestID: $requestID, destCollID: $collectionID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
MoveRestTeamRequestDocument,
|
||||
MoveRestTeamRequestMutation,
|
||||
MoveRestTeamRequestMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type MoveRestTeamRequestErrors =
|
||||
| "team_req/not_found"
|
||||
| "team_req/invalid_target_id"
|
||||
|
||||
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
|
||||
runMutation<
|
||||
MoveRestTeamRequestMutation,
|
||||
MoveRestTeamRequestMutationVariables,
|
||||
MoveRestTeamRequestErrors
|
||||
>(MoveRestTeamRequestDocument, {
|
||||
requestID,
|
||||
collectionID,
|
||||
})
|
||||
@@ -1,434 +0,0 @@
|
||||
import {
|
||||
User,
|
||||
getAuth,
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
GithubAuthProvider,
|
||||
OAuthProvider,
|
||||
signInWithEmailAndPassword as signInWithEmailAndPass,
|
||||
isSignInWithEmailLink as isSignInWithEmailLinkFB,
|
||||
fetchSignInMethodsForEmail,
|
||||
sendSignInLinkToEmail,
|
||||
signInWithEmailLink as signInWithEmailLinkFB,
|
||||
ActionCodeSettings,
|
||||
signOut,
|
||||
linkWithCredential,
|
||||
AuthCredential,
|
||||
AuthError,
|
||||
UserCredential,
|
||||
updateProfile,
|
||||
updateEmail,
|
||||
sendEmailVerification,
|
||||
reauthenticateWithCredential,
|
||||
} from "firebase/auth"
|
||||
import {
|
||||
onSnapshot,
|
||||
getFirestore,
|
||||
setDoc,
|
||||
doc,
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import { BehaviorSubject, filter, Subject, Subscription } from "rxjs"
|
||||
import {
|
||||
setLocalConfig,
|
||||
getLocalConfig,
|
||||
removeLocalConfig,
|
||||
} from "~/newstore/localpersistence"
|
||||
|
||||
export type HoppUser = User & {
|
||||
provider?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export type AuthEvent =
|
||||
| { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication
|
||||
| { event: "login"; user: HoppUser } // We are authenticated
|
||||
| { event: "logout" } // No authentication and we have no previous state
|
||||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Token has been updated
|
||||
|
||||
/**
|
||||
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
|
||||
*/
|
||||
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
/**
|
||||
* A BehaviorSubject emitting the current idToken
|
||||
*/
|
||||
export const authIdToken$ = new BehaviorSubject<string | null>(null)
|
||||
|
||||
/**
|
||||
* A subject that emits events related to authentication flows
|
||||
*/
|
||||
export const authEvents$ = new Subject<AuthEvent>()
|
||||
|
||||
/**
|
||||
* Like currentUser$ but also gives probable user value
|
||||
*/
|
||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
|
||||
/**
|
||||
* Resolves when the probable login resolves into proper login
|
||||
*/
|
||||
export const waitProbableLoginToConfirm = () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (authIdToken$.value) resolve()
|
||||
|
||||
if (!probableUser$.value) reject(new Error("no_probable_user"))
|
||||
|
||||
let sub: Subscription | null = null
|
||||
|
||||
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => {
|
||||
sub?.unsubscribe()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Initializes the firebase authentication related subjects
|
||||
*/
|
||||
export function initAuth() {
|
||||
const auth = getAuth()
|
||||
const firestore = getFirestore()
|
||||
|
||||
let extraSnapshotStop: (() => void) | null = null
|
||||
|
||||
probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null"))
|
||||
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
/** Whether the user was logged in before */
|
||||
const wasLoggedIn = currentUser$.value !== null
|
||||
|
||||
if (user) {
|
||||
probableUser$.next(user)
|
||||
} else {
|
||||
probableUser$.next(null)
|
||||
removeLocalConfig("login_state")
|
||||
}
|
||||
|
||||
if (!user && extraSnapshotStop) {
|
||||
extraSnapshotStop()
|
||||
extraSnapshotStop = null
|
||||
} else if (user) {
|
||||
// Merge all the user info from all the authenticated providers
|
||||
user.providerData.forEach((profile) => {
|
||||
if (!profile) return
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: profile.providerId,
|
||||
name: profile.displayName,
|
||||
email: profile.email,
|
||||
photoUrl: profile.photoURL,
|
||||
uid: profile.uid,
|
||||
}
|
||||
|
||||
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
|
||||
(e) => console.error("error updating", us, e)
|
||||
)
|
||||
})
|
||||
|
||||
extraSnapshotStop = onSnapshot(
|
||||
doc(firestore, "users", user.uid),
|
||||
(doc) => {
|
||||
const data = doc.data()
|
||||
|
||||
const userUpdate: HoppUser = user
|
||||
|
||||
if (data) {
|
||||
// Write extra provider data
|
||||
userUpdate.provider = data.provider
|
||||
userUpdate.accessToken = data.accessToken
|
||||
}
|
||||
|
||||
currentUser$.next(userUpdate)
|
||||
}
|
||||
)
|
||||
}
|
||||
currentUser$.next(user)
|
||||
|
||||
// User wasn't found before, but now is there (login happened)
|
||||
if (!wasLoggedIn && user) {
|
||||
authEvents$.next({
|
||||
event: "login",
|
||||
user: currentUser$.value!,
|
||||
})
|
||||
} else if (wasLoggedIn && !user) {
|
||||
// User was found before, but now is not there (logout happened)
|
||||
authEvents$.next({
|
||||
event: "logout",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onIdTokenChanged(auth, async (user) => {
|
||||
if (user) {
|
||||
authIdToken$.next(await user.getIdToken())
|
||||
|
||||
authEvents$.next({
|
||||
event: "authTokenUpdate",
|
||||
newToken: authIdToken$.value,
|
||||
user: currentUser$.value!, // Force not-null because user is defined
|
||||
})
|
||||
|
||||
setLocalConfig("login_state", JSON.stringify(user))
|
||||
} else {
|
||||
authIdToken$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuthIDToken(): string | null {
|
||||
return authIdToken$.getValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Google
|
||||
*/
|
||||
export async function signInUserWithGoogle() {
|
||||
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Github
|
||||
*/
|
||||
export async function signInUserWithGithub() {
|
||||
return await signInWithPopup(
|
||||
getAuth(),
|
||||
new GithubAuthProvider().addScope("gist")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Microsoft
|
||||
*/
|
||||
export async function signInUserWithMicrosoft() {
|
||||
return await signInWithPopup(getAuth(), new OAuthProvider("microsoft.com"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with email and password
|
||||
*/
|
||||
export async function signInWithEmailAndPassword(
|
||||
email: string,
|
||||
password: string
|
||||
) {
|
||||
return await signInWithEmailAndPass(getAuth(), email, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sign in methods for a given email address
|
||||
*
|
||||
* @param email - Email to get the methods of
|
||||
*
|
||||
* @returns Promise for string array of the auth provider methods accessible
|
||||
*/
|
||||
export async function getSignInMethodsForEmail(email: string) {
|
||||
return await fetchSignInMethodsForEmail(getAuth(), email)
|
||||
}
|
||||
|
||||
export async function linkWithFBCredential(
|
||||
user: User,
|
||||
credential: AuthCredential
|
||||
) {
|
||||
return await linkWithCredential(user, credential)
|
||||
}
|
||||
|
||||
/**
|
||||
* Links account with another account given in a auth/account-exists-with-different-credential error
|
||||
*
|
||||
* @param error - Error caught after trying to login
|
||||
*
|
||||
* @returns Promise of UserCredential
|
||||
*/
|
||||
export async function linkWithFBCredentialFromAuthError(error: unknown) {
|
||||
// credential is not null since this function is called after an auth/account-exists-with-different-credential error, ie credentials actually exist
|
||||
const credentials = OAuthProvider.credentialFromError(error as AuthError)!
|
||||
|
||||
const otherLinkedProviders = (
|
||||
await getSignInMethodsForEmail((error as AuthError).customData.email!)
|
||||
).filter((providerId) => credentials.providerId !== providerId)
|
||||
|
||||
let user: User | null = null
|
||||
|
||||
if (otherLinkedProviders.indexOf("google.com") >= -1) {
|
||||
user = (await signInUserWithGoogle()).user
|
||||
} else if (otherLinkedProviders.indexOf("github.com") >= -1) {
|
||||
user = (await signInUserWithGithub()).user
|
||||
} else if (otherLinkedProviders.indexOf("microsoft.com") >= -1) {
|
||||
user = (await signInUserWithMicrosoft()).user
|
||||
}
|
||||
|
||||
// user is not null since going through each provider will return a user
|
||||
return await linkWithCredential(user!, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with the signin link to the user
|
||||
*
|
||||
* @param email - Email to send the email to
|
||||
* @param actionCodeSettings - The settings to apply to the link
|
||||
*/
|
||||
export async function signInWithEmail(
|
||||
email: string,
|
||||
actionCodeSettings: ActionCodeSettings
|
||||
) {
|
||||
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and returns whether the sign in link is an email link
|
||||
*
|
||||
* @param url - The URL to look in
|
||||
*/
|
||||
export function isSignInWithEmailLink(url: string) {
|
||||
return isSignInWithEmailLinkFB(getAuth(), url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with sign in with email link
|
||||
*
|
||||
* @param email - Email to log in to
|
||||
* @param url - The action URL which is used to validate login
|
||||
*/
|
||||
export async function signInWithEmailLink(email: string, url: string) {
|
||||
return await signInWithEmailLinkFB(getAuth(), email, url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the user
|
||||
*/
|
||||
export async function signOutUser() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
await signOut(getAuth())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider id and relevant provider auth token
|
||||
* as user metadata
|
||||
*
|
||||
* @param id - The provider ID
|
||||
* @param token - The relevant auth token for the given provider
|
||||
*/
|
||||
export async function setProviderInfo(id: string, token: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: id,
|
||||
accessToken: token,
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid),
|
||||
us
|
||||
).catch((e) => console.error("error updating", us, e))
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's display name
|
||||
*
|
||||
* @param name - The new display name
|
||||
*/
|
||||
export async function setDisplayName(name: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
const us = {
|
||||
displayName: name,
|
||||
}
|
||||
|
||||
try {
|
||||
await updateProfile(currentUser$.value, us)
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send user's email address verification mail
|
||||
*/
|
||||
export async function verifyEmailAddress() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
try {
|
||||
await sendEmailVerification(currentUser$.value)
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's email address
|
||||
*
|
||||
* @param email - The new email address
|
||||
*/
|
||||
export async function setEmailAddress(email: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
try {
|
||||
await updateEmail(currentUser$.value, email)
|
||||
} catch (e) {
|
||||
await reauthenticateUser()
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthenticate the user with the given credential
|
||||
*/
|
||||
async function reauthenticateUser() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
const currentAuthMethod = currentUser$.value.provider
|
||||
let credential
|
||||
if (currentAuthMethod === "google.com") {
|
||||
const result = await signInUserWithGithub()
|
||||
credential = GithubAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "github.com") {
|
||||
const result = await signInUserWithGoogle()
|
||||
credential = GoogleAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "microsoft.com") {
|
||||
const result = await signInUserWithMicrosoft()
|
||||
credential = OAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "password") {
|
||||
const email = prompt(
|
||||
"Reauthenticate your account using your current email:"
|
||||
)
|
||||
const actionCodeSettings = {
|
||||
url: `${process.env.BASE_URL}/enter`,
|
||||
handleCodeInApp: true,
|
||||
}
|
||||
await signInWithEmail(email as string, actionCodeSettings)
|
||||
.then(() =>
|
||||
alert(
|
||||
`Check your inbox - we sent an email to ${email}. It contains a magic link that will reauthenticate your account.`
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
alert(`Error: ${e.message}`)
|
||||
console.error(e)
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await reauthenticateWithCredential(
|
||||
currentUser$.value,
|
||||
credential as AuthCredential
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function getGithubCredentialFromResult(result: UserCredential) {
|
||||
return GithubAuthProvider.credentialFromResult(result)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
audit,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
from,
|
||||
map,
|
||||
Subscription,
|
||||
} from "rxjs"
|
||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { currentUser$, HoppUser } from "./auth"
|
||||
import { restRequest$ } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
* Writes a request to a user's firestore sync
|
||||
*
|
||||
* @param user The user to write to
|
||||
* @param request The request to write to the request sync
|
||||
*/
|
||||
function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
|
||||
const req = cloneDeep(request)
|
||||
|
||||
// Remove FormData entries because those can't be stored on Firestore
|
||||
if (req.body.contentType === "multipart/form-data") {
|
||||
req.body.body = req.body.body.map((formData) => {
|
||||
if (!formData.isFile) return formData
|
||||
|
||||
return {
|
||||
active: formData.active,
|
||||
isFile: false,
|
||||
key: formData.key,
|
||||
value: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
return setDoc(doc(getFirestore(), "users", user.uid, "requests", "rest"), req)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the synced request from the firestore sync
|
||||
*
|
||||
* @returns Fetched request object if exists else null
|
||||
*/
|
||||
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||
const currentUser = currentUser$.value
|
||||
|
||||
if (!currentUser)
|
||||
throw new Error("Cannot load request from sync without login")
|
||||
|
||||
const fbDoc = await getDoc(
|
||||
doc(getFirestore(), "users", currentUser.uid, "requests", "rest")
|
||||
)
|
||||
|
||||
const data = fbDoc.data()
|
||||
|
||||
if (!data) return null
|
||||
else return translateToNewRequest(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs sync of the REST Request session with Firestore.
|
||||
*
|
||||
* @returns A subscription to the sync observable stream.
|
||||
* Unsubscribe to stop syncing.
|
||||
*/
|
||||
export function startRequestSync(): Subscription {
|
||||
const sub = combineLatest([
|
||||
currentUser$,
|
||||
restRequest$.pipe(distinctUntilChanged()),
|
||||
])
|
||||
.pipe(
|
||||
map(([user, request]) =>
|
||||
user ? from(writeCurrentRequest(user, request)) : EMPTY
|
||||
),
|
||||
audit((x) => x)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// NOTE: This subscription should be kept
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { authIdToken$ } from "../fb/auth"
|
||||
import { runGQLQuery } from "../backend/GQLClient"
|
||||
import { GetUserInfoDocument } from "../backend/graphql"
|
||||
|
||||
/*
|
||||
* This file deals with interfacing data provided by the
|
||||
* Hoppscotch Backend server
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the information provided about a user
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/**
|
||||
* UID of the user
|
||||
*/
|
||||
uid: string
|
||||
/**
|
||||
* Displayable name of the user (or null if none available)
|
||||
*/
|
||||
displayName: string | null
|
||||
/**
|
||||
* Email of the user (or null if none available)
|
||||
*/
|
||||
email: string | null
|
||||
/**
|
||||
* URL to the profile photo of the user (or null if none available)
|
||||
*/
|
||||
photoURL: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable subject onto the currently logged in user info (is null if not logged in)
|
||||
*/
|
||||
export const currentUserInfo$ = new BehaviorSubject<UserInfo | null>(null)
|
||||
|
||||
/**
|
||||
* Initializes the currenUserInfo$ view and sets up its update mechanism
|
||||
*/
|
||||
export function initUserInfo() {
|
||||
authIdToken$.subscribe((token) => {
|
||||
if (token) {
|
||||
updateUserInfo()
|
||||
} else {
|
||||
currentUserInfo$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the actual user info fetching
|
||||
*/
|
||||
async function updateUserInfo() {
|
||||
const result = await runGQLQuery({
|
||||
query: GetUserInfoDocument,
|
||||
})
|
||||
|
||||
currentUserInfo$.next(
|
||||
pipe(
|
||||
result,
|
||||
E.matchW(
|
||||
() => null,
|
||||
(x) => ({
|
||||
uid: x.me.uid,
|
||||
displayName: x.me.displayName ?? null,
|
||||
email: x.me.email ?? null,
|
||||
photoURL: x.me.photoURL ?? null,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { createApp } from "vue"
|
||||
import { setupLocalPersistence } from "./newstore/localpersistence"
|
||||
import { performMigrations } from "./helpers/migrations"
|
||||
import { initializeFirebase } from "./helpers/fb"
|
||||
import { initUserInfo } from "./helpers/teams/BackendUserInfo"
|
||||
import { HOPP_MODULES } from "@modules/."
|
||||
|
||||
import "virtual:windi.css"
|
||||
import "../assets/scss/themes.scss"
|
||||
import "../assets/scss/styles.scss"
|
||||
import "nprogress/nprogress.css"
|
||||
|
||||
import App from "./App.vue"
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// Some basic work that needs to be done before module inits even
|
||||
initializeFirebase()
|
||||
setupLocalPersistence()
|
||||
performMigrations()
|
||||
initUserInfo()
|
||||
|
||||
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
||||
|
||||
app.mount("#app")
|
||||
|
||||
console.info(
|
||||
"%cWe ❤︎ open source!",
|
||||
"background-color:white;padding:8px 16px;border-radius:8px;font-size:32px;color:red;"
|
||||
)
|
||||
console.info(
|
||||
"%cContribute: https://github.com/hoppscotch/hoppscotch",
|
||||
"background-color:black;padding:4px 8px;border-radius:8px;font-size:16px;color:white;"
|
||||
)
|
||||
@@ -1,710 +0,0 @@
|
||||
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
|
||||
import { Ref } from "vue"
|
||||
import {
|
||||
FormDataKeyValue,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
RESTReqSchemaVersion,
|
||||
HoppRESTAuth,
|
||||
ValidContentTypes,
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
|
||||
|
||||
type RESTSession = {
|
||||
request: HoppRESTRequest
|
||||
response: HoppRESTResponse | null
|
||||
testResults: HoppTestResult | null
|
||||
saveContext: HoppRequestSaveContext | null
|
||||
}
|
||||
|
||||
export const getDefaultRESTRequest = (): HoppRESTRequest => ({
|
||||
v: RESTReqSchemaVersion,
|
||||
endpoint: "https://echo.hoppscotch.io",
|
||||
name: "Untitled request",
|
||||
params: [],
|
||||
headers: [],
|
||||
method: "GET",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
})
|
||||
|
||||
const defaultRESTSession: RESTSession = {
|
||||
request: getDefaultRESTRequest(),
|
||||
response: null,
|
||||
testResults: null,
|
||||
saveContext: null,
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
|
||||
return {
|
||||
request: req,
|
||||
}
|
||||
},
|
||||
setRequestName(curr: RESTSession, { newName }: { newName: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
name: newName,
|
||||
},
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: entries,
|
||||
},
|
||||
}
|
||||
},
|
||||
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: [...curr.request.params, newParam],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateParam(
|
||||
curr: RESTSession,
|
||||
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
|
||||
) {
|
||||
const newParams = curr.request.params.map((param, i) => {
|
||||
if (i === index) return updatedParam
|
||||
else return param
|
||||
})
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: newParams,
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteParam(curr: RESTSession, { index }: { index: number }) {
|
||||
const newParams = curr.request.params.filter((_x, i) => i !== index)
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: newParams,
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllParams(curr: RESTSession) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
method: newMethod,
|
||||
},
|
||||
}
|
||||
},
|
||||
setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: entries,
|
||||
},
|
||||
}
|
||||
},
|
||||
addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: [...curr.request.headers, entry],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateHeader(
|
||||
curr: RESTSession,
|
||||
{ index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.map((header, i) => {
|
||||
if (i === index) return updatedEntry
|
||||
else return header
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteHeader(curr: RESTSession, { index }: { index: number }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.filter((_, i) => i !== index),
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllHeaders(curr: RESTSession) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
auth: newAuth,
|
||||
},
|
||||
}
|
||||
},
|
||||
setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
preRequestScript: newScript,
|
||||
},
|
||||
}
|
||||
},
|
||||
setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
testScript: newScript,
|
||||
},
|
||||
}
|
||||
},
|
||||
setContentType(
|
||||
curr: RESTSession,
|
||||
{ newContentType }: { newContentType: ValidContentTypes | null }
|
||||
) {
|
||||
// TODO: persist body evenafter switching content typees
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: applyBodyTransition(curr.request.body, newContentType),
|
||||
},
|
||||
}
|
||||
},
|
||||
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: [...curr.request.body.body, entry],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: curr.request.body.body.filter((_, i) => i !== index),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
updateFormDataEntry(
|
||||
curr: RESTSession,
|
||||
{ index, entry }: { index: number; entry: FormDataKeyValue }
|
||||
) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllFormDataEntries(curr: RESTSession) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: newBody,
|
||||
},
|
||||
}
|
||||
},
|
||||
updateResponse(
|
||||
_curr: RESTSession,
|
||||
{ updatedRes }: { updatedRes: HoppRESTResponse | null }
|
||||
) {
|
||||
return {
|
||||
response: updatedRes,
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
clearResponse(_curr: RESTSession) {
|
||||
return {
|
||||
response: null,
|
||||
}
|
||||
},
|
||||
setTestResults(
|
||||
_curr: RESTSession,
|
||||
{ newResults }: { newResults: HoppTestResult | null }
|
||||
) {
|
||||
return {
|
||||
testResults: newResults,
|
||||
}
|
||||
},
|
||||
setSaveContext(
|
||||
_,
|
||||
{ newContext }: { newContext: HoppRequestSaveContext | null }
|
||||
) {
|
||||
return {
|
||||
saveContext: newContext,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
|
||||
|
||||
export function getRESTRequest() {
|
||||
return restSessionStore.subject$.value.request
|
||||
}
|
||||
|
||||
export function setRESTRequest(
|
||||
req: HoppRESTRequest,
|
||||
saveContext?: HoppRequestSaveContext | null
|
||||
) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
req,
|
||||
},
|
||||
})
|
||||
|
||||
if (saveContext) setRESTSaveContext(saveContext)
|
||||
}
|
||||
|
||||
export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setSaveContext",
|
||||
payload: {
|
||||
newContext: saveContext,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getRESTSaveContext() {
|
||||
return restSessionStore.value.saveContext
|
||||
}
|
||||
|
||||
export function resetRESTRequest() {
|
||||
setRESTRequest(getDefaultRESTRequest())
|
||||
}
|
||||
|
||||
export function setRESTEndpoint(newEndpoint: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTRequestName(newName: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequestName",
|
||||
payload: {
|
||||
newName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTParams(entries: HoppRESTParam[]) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setParams",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTParam(newParam: HoppRESTParam) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addParam",
|
||||
payload: {
|
||||
newParam,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateParam",
|
||||
payload: {
|
||||
updatedParam,
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRESTParam(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteParam",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllRESTParams() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllParams",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTMethod(newMethod: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateMethod",
|
||||
payload: {
|
||||
newMethod,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTHeaders(entries: HoppRESTHeader[]) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTHeader(entry: HoppRESTHeader) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addHeader",
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateHeader",
|
||||
payload: {
|
||||
index,
|
||||
updatedEntry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRESTHeader(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteHeader",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllRESTHeaders() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllHeaders",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTAuth(newAuth: HoppRESTAuth) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setAuth",
|
||||
payload: {
|
||||
newAuth,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTPreRequestScript(newScript: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setPreRequestScript",
|
||||
payload: {
|
||||
newScript,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTTestScript(newScript: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setTestScript",
|
||||
payload: {
|
||||
newScript,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequestBody",
|
||||
payload: {
|
||||
newBody,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateResponse",
|
||||
payload: {
|
||||
updatedRes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function clearRESTResponse() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "clearResponse",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTTestResults(newResults: HoppTestResult | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setTestResults",
|
||||
payload: {
|
||||
newResults,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addFormDataEntry(entry: FormDataKeyValue) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addFormDataEntry",
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteFormDataEntry(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteFormDataEntry",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateFormDataEntry",
|
||||
payload: {
|
||||
index,
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTContentType(newContentType: ValidContentTypes | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setContentType",
|
||||
payload: {
|
||||
newContentType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllFormDataEntries() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllFormDataEntries",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export const restSaveContext$ = restSessionStore.subject$.pipe(
|
||||
pluck("saveContext"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restRequest$ = restSessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restRequestName$ = restRequest$.pipe(
|
||||
pluck("name"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restEndpoint$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restParams$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "params"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restActiveParamsCount$ = restParams$.pipe(
|
||||
map(
|
||||
(params) =>
|
||||
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
|
||||
)
|
||||
)
|
||||
|
||||
export const restMethod$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "method"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restHeaders$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "headers"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restActiveHeadersCount$ = restHeaders$.pipe(
|
||||
map(
|
||||
(params) =>
|
||||
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
|
||||
)
|
||||
)
|
||||
|
||||
export const restAuth$ = restRequest$.pipe(pluck("auth"))
|
||||
|
||||
export const restPreRequestScript$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "preRequestScript"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restContentType$ = restRequest$.pipe(
|
||||
pluck("body", "contentType"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restTestScript$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "testScript"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restReqBody$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "body"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restResponse$ = restSessionStore.subject$.pipe(
|
||||
pluck("response"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const completedRESTResponse$ = restResponse$.pipe(
|
||||
filter(
|
||||
(res) =>
|
||||
res !== null &&
|
||||
res.type !== "loading" &&
|
||||
res.type !== "network_fail" &&
|
||||
res.type !== "script_fail"
|
||||
)
|
||||
)
|
||||
|
||||
export const restTestResults$ = restSessionStore.subject$.pipe(
|
||||
pluck("testResults"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/**
|
||||
* A Vue 3 composable function that gives access to a ref
|
||||
* which is updated to the preRequestScript value in the store.
|
||||
* The ref value is kept in sync with the store and all writes
|
||||
* to the ref are dispatched to the store as `setPreRequestScript`
|
||||
* dispatches.
|
||||
*/
|
||||
export function usePreRequestScript(): Ref<string> {
|
||||
return useStream(
|
||||
restPreRequestScript$,
|
||||
restSessionStore.value.request.preRequestScript,
|
||||
(value) => {
|
||||
setRESTPreRequestScript(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vue 3 composable function that gives access to a ref
|
||||
* which is updated to the testScript value in the store.
|
||||
* The ref value is kept in sync with the store and all writes
|
||||
* to the ref are dispatched to the store as `setTestScript`
|
||||
* dispatches.
|
||||
*/
|
||||
export function useTestScript(): Ref<string> {
|
||||
return useStream(
|
||||
restTestScript$,
|
||||
restSessionStore.value.request.testScript,
|
||||
(value) => {
|
||||
setRESTTestScript(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
|
||||
return useStream(
|
||||
restReqBody$,
|
||||
restSessionStore.value.request.body,
|
||||
setRESTReqBody
|
||||
)
|
||||
}
|
||||
|
||||
export function useRESTRequestName(): Ref<string> {
|
||||
return useStream(
|
||||
restRequestName$,
|
||||
restSessionStore.value.request.name,
|
||||
setRESTRequestName
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center min-h-screen">
|
||||
<div v-if="signingInWithEmail">
|
||||
<SmartSpinner />
|
||||
<div class="mt-2 text-sm text-secondaryLight">
|
||||
{{ t("state.loading") }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<AppLogo class="w-16 h-16 rounded" />
|
||||
</div>
|
||||
<pre v-if="error" class="mt-4 text-secondaryLight">{{ error }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { initializeFirebase } from "~/helpers/fb"
|
||||
import { isSignInWithEmailLink, signInWithEmailLink } from "~/helpers/fb/auth"
|
||||
import { getLocalConfig, removeLocalConfig } from "~/newstore/localpersistence"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signingInWithEmail: false,
|
||||
error: null,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
initializeFirebase()
|
||||
},
|
||||
async mounted() {
|
||||
if (isSignInWithEmailLink(window.location.href)) {
|
||||
this.signingInWithEmail = true
|
||||
|
||||
let email = getLocalConfig("emailForSignIn")
|
||||
|
||||
if (!email) {
|
||||
email = window.prompt(
|
||||
"Please provide your email for confirmation"
|
||||
) as string
|
||||
}
|
||||
|
||||
await signInWithEmailLink(email, window.location.href)
|
||||
.then(() => {
|
||||
removeLocalConfig("emailForSignIn")
|
||||
this.$router.push({ path: "/" })
|
||||
})
|
||||
.catch((e) => {
|
||||
this.signingInWithEmail = false
|
||||
this.error = e.message
|
||||
})
|
||||
.finally(() => {
|
||||
this.signingInWithEmail = false
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: empty
|
||||
</route>
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="http">
|
||||
<template #primary>
|
||||
<HttpRequest />
|
||||
<HttpRequestOptions />
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpResponse />
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<HttpSidebar />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
Ref,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue"
|
||||
import type { Subscription } from "rxjs"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
HoppRESTAuthOAuth2,
|
||||
safelyExtractRESTRequest,
|
||||
isEqualHoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import {
|
||||
getRESTRequest,
|
||||
setRESTRequest,
|
||||
setRESTAuth,
|
||||
restAuth$,
|
||||
getDefaultRESTRequest,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import { loadRequestFromSync, startRequestSync } from "~/helpers/fb/request"
|
||||
import { oauthRedirect } from "~/helpers/oauth"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
function bindRequestToURLParams() {
|
||||
const route = useRoute()
|
||||
// Get URL parameters and set that as the request
|
||||
onMounted(() => {
|
||||
const query = route.query
|
||||
// If query params are empty, or contains code or error param (these are from Oauth Redirect)
|
||||
// We skip URL params parsing
|
||||
if (Object.keys(query).length === 0 || query.code || query.error) return
|
||||
setRESTRequest(
|
||||
safelyExtractRESTRequest(
|
||||
translateExtURLParams(query),
|
||||
getDefaultRESTRequest()
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function oAuthURL() {
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const tokenInfo = await oauthRedirect()
|
||||
if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||
if (typeof tokenInfo === "object") {
|
||||
oauth2Token.value = tokenInfo.access_token
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (_) {}
|
||||
})
|
||||
}
|
||||
|
||||
function setupRequestSync(
|
||||
confirmSync: Ref<boolean>,
|
||||
requestForSync: Ref<HoppRESTRequest | null>
|
||||
) {
|
||||
const route = useRoute()
|
||||
|
||||
// Subscription to request sync
|
||||
let sub: Subscription | null = null
|
||||
|
||||
// Load request on login resolve and start sync
|
||||
onLoggedIn(async () => {
|
||||
if (
|
||||
Object.keys(route.query).length === 0 &&
|
||||
!(route.query.code || route.query.error)
|
||||
) {
|
||||
const request = await loadRequestFromSync()
|
||||
if (request) {
|
||||
if (!isEqualHoppRESTRequest(request, getRESTRequest())) {
|
||||
requestForSync.value = request
|
||||
confirmSync.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub = startRequestSync()
|
||||
})
|
||||
|
||||
// Stop subscription to stop syncing
|
||||
onBeforeUnmount(() => {
|
||||
sub?.unsubscribe()
|
||||
})
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const requestForSync = ref<HoppRESTRequest | null>(null)
|
||||
|
||||
const confirmSync = ref(false)
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
watch(confirmSync, (newValue) => {
|
||||
if (newValue) {
|
||||
toast.show(`${t("confirm.sync")}`, {
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.yes")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
syncRequest()
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `${t("action.no")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const syncRequest = () => {
|
||||
setRESTRequest(
|
||||
safelyExtractRESTRequest(requestForSync.value!, getDefaultRESTRequest())
|
||||
)
|
||||
}
|
||||
|
||||
setupRequestSync(confirmSync, requestForSync)
|
||||
bindRequestToURLParams()
|
||||
oAuthURL()
|
||||
|
||||
return {
|
||||
confirmSync,
|
||||
syncRequest,
|
||||
oAuthURL,
|
||||
requestForSync,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="mqtt">
|
||||
<template #primary>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary"
|
||||
>
|
||||
<div class="inline-flex flex-1 space-x-2">
|
||||
<input
|
||||
id="mqtt-url"
|
||||
v-model="url"
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
|
||||
:placeholder="t('mqtt.url')"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="connect"
|
||||
:disabled="!isUrlValid"
|
||||
class="w-32"
|
||||
:label="
|
||||
connectionState === 'DISCONNECTED'
|
||||
? t('action.connect')
|
||||
: t('action.disconnect')
|
||||
"
|
||||
:loading="connectionState === 'CONNECTING'"
|
||||
@click="toggleConnection"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="mqtt-username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
class="input"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
<input
|
||||
id="mqtt-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
spellcheck="false"
|
||||
class="input"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<RealtimeLog
|
||||
:title="t('mqtt.log')"
|
||||
:log="log"
|
||||
@delete="clearLogEntries()"
|
||||
/>
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label for="pubTopic" class="font-semibold text-secondaryLight">
|
||||
{{ t("mqtt.topic") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4">
|
||||
<input
|
||||
id="pubTopic"
|
||||
v-model="pubTopic"
|
||||
class="input"
|
||||
:placeholder="t('mqtt.topic_name')"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label for="mqtt-message" class="font-semibold text-secondaryLight">
|
||||
{{ t("mqtt.communication") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4 space-x-2">
|
||||
<input
|
||||
id="mqtt-message"
|
||||
v-model="message"
|
||||
class="input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="t('mqtt.message')"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="publish"
|
||||
name="get"
|
||||
:disabled="!canPublish"
|
||||
:label="t('mqtt.publish')"
|
||||
@click="publish"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
|
||||
>
|
||||
<label for="subTopic" class="font-semibold text-secondaryLight">
|
||||
{{ t("mqtt.topic") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4 space-x-2">
|
||||
<input
|
||||
id="subTopic"
|
||||
v-model="subTopic"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="t('mqtt.topic_name')"
|
||||
spellcheck="false"
|
||||
class="input"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="subscribe"
|
||||
name="get"
|
||||
:disabled="!canSubscribe"
|
||||
:label="
|
||||
subscriptionState ? t('mqtt.unsubscribe') : t('mqtt.subscribe')
|
||||
"
|
||||
reverse
|
||||
@click="toggleSubscription"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
|
||||
import { debounce } from "lodash-es"
|
||||
import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
useStreamSubscriber,
|
||||
} from "@composables/stream"
|
||||
import {
|
||||
addMQTTLogLine,
|
||||
MQTTConn$,
|
||||
MQTTEndpoint$,
|
||||
MQTTLog$,
|
||||
setMQTTConn,
|
||||
setMQTTEndpoint,
|
||||
setMQTTLog,
|
||||
} from "~/newstore/MQTTSession"
|
||||
import RegexWorker from "@workers/regex?worker"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
|
||||
const log = useStream(MQTTLog$, [], setMQTTLog)
|
||||
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
|
||||
const connectionState = useReadonlyStream(
|
||||
socket.value.connectionState$,
|
||||
"DISCONNECTED"
|
||||
)
|
||||
const subscriptionState = useReadonlyStream(
|
||||
socket.value.subscriptionState$,
|
||||
false
|
||||
)
|
||||
|
||||
const isUrlValid = ref(true)
|
||||
const pubTopic = ref("")
|
||||
const subTopic = ref("")
|
||||
const message = ref("")
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
|
||||
let worker: Worker
|
||||
|
||||
const canPublish = computed(
|
||||
() =>
|
||||
pubTopic.value !== "" &&
|
||||
message.value !== "" &&
|
||||
connectionState.value === "CONNECTED"
|
||||
)
|
||||
const canSubscribe = computed(
|
||||
() => subTopic.value !== "" && connectionState.value === "CONNECTED"
|
||||
)
|
||||
|
||||
const workerResponseHandler = ({
|
||||
data,
|
||||
}: {
|
||||
data: { url: string; result: boolean }
|
||||
}) => {
|
||||
if (data.url === url.value) isUrlValid.value = data.result
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
worker = new RegexWorker()
|
||||
worker.addEventListener("message", workerResponseHandler)
|
||||
|
||||
subscribeToStream(socket.value.event$, (event) => {
|
||||
switch (event?.type) {
|
||||
case "CONNECTING":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: undefined,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
case "CONNECTED":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
toast.success(`${t("state.connected")}`)
|
||||
break
|
||||
|
||||
case "MESSAGE_SENT":
|
||||
addMQTTLogLine({
|
||||
prefix: `${event.message.topic}`,
|
||||
payload: event.message.message,
|
||||
source: "client",
|
||||
ts: Date.now(),
|
||||
})
|
||||
break
|
||||
|
||||
case "MESSAGE_RECEIVED":
|
||||
addMQTTLogLine({
|
||||
prefix: `${event.message.topic}`,
|
||||
payload: event.message.message,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "SUBSCRIBED":
|
||||
addMQTTLogLine({
|
||||
payload: subscriptionState.value
|
||||
? `${t("state.subscribed_success", { topic: subTopic.value })}`
|
||||
: `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "SUBSCRIPTION_FAILED":
|
||||
addMQTTLogLine({
|
||||
payload: subscriptionState.value
|
||||
? `${t("state.subscribed_failed", { topic: subTopic.value })}`
|
||||
: `${t("state.unsubscribed_failed", { topic: subTopic.value })}`,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "ERROR":
|
||||
addMQTTLogLine({
|
||||
payload: getI18nError(event.error),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "DISCONNECTED":
|
||||
addMQTTLogLine({
|
||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
toast.error(`${t("state.disconnected")}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const debouncer = debounce(function () {
|
||||
worker.postMessage({ type: "ws", url: url.value })
|
||||
}, 1000)
|
||||
|
||||
watch(url, (newUrl) => {
|
||||
if (newUrl) debouncer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
worker.terminate()
|
||||
})
|
||||
|
||||
// METHODS
|
||||
const toggleConnection = () => {
|
||||
// If it is connecting:
|
||||
if (connectionState.value === "DISCONNECTED") {
|
||||
return socket.value.connect(url.value, username.value, password.value)
|
||||
}
|
||||
// Otherwise, it's disconnecting.
|
||||
socket.value.disconnect()
|
||||
}
|
||||
const publish = () => {
|
||||
socket.value?.publish(pubTopic.value, message.value)
|
||||
}
|
||||
const toggleSubscription = () => {
|
||||
if (subscriptionState.value) {
|
||||
socket.value.unsubscribe(subTopic.value)
|
||||
} else {
|
||||
socket.value.subscribe(subTopic.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getI18nError = (error: MQTTError): string => {
|
||||
if (typeof error === "string") return error
|
||||
|
||||
switch (error.type) {
|
||||
case "CONNECTION_NOT_ESTABLISHED":
|
||||
return t("state.connection_lost").toString()
|
||||
case "SUBSCRIPTION_FAILED":
|
||||
return t("state.mqtt_subscription_failed", {
|
||||
topic: error.topic,
|
||||
}).toString()
|
||||
case "PUBLISH_ERROR":
|
||||
return t("state.publish_error", { topic: error.topic }).toString()
|
||||
case "CONNECTION_LOST":
|
||||
return t("state.connection_lost").toString()
|
||||
case "CONNECTION_FAILED":
|
||||
return t("state.connection_failed").toString()
|
||||
default:
|
||||
return t("state.disconnected_from", { name: url.value }).toString()
|
||||
}
|
||||
}
|
||||
const clearLogEntries = () => {
|
||||
log.value = []
|
||||
}
|
||||
</script>
|
||||
@@ -1,4 +1,17 @@
|
||||
# Hoppscotch CLI <sup>ALPHA</sup>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
A CLI to run Hoppscotch test scripts in CI environments.
|
||||
|
||||
@@ -33,7 +46,7 @@ hopp [options or commands] arguments
|
||||
|
||||
#### Options:
|
||||
##### `-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
|
||||
{
|
||||
"ENV1":"value1",
|
||||
@@ -41,7 +54,7 @@ hopp [options or commands] arguments
|
||||
}
|
||||
```
|
||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||
|
||||
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
|
||||
## Install
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
@@ -36,8 +36,8 @@
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^0.4.4",
|
||||
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@swc/core": "^1.2.181",
|
||||
"@types/axios": "^0.14.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"io-ts": "^2.2.16",
|
||||
"jest": "^27.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier": "^2.8.4",
|
||||
"qs": "^6.10.3",
|
||||
"ts-jest": "^27.1.4",
|
||||
"tsup": "^5.12.7",
|
||||
|
||||
@@ -5,42 +5,52 @@ import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const cmd = `node ./bin/hopp test`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const cmd = `node ./bin/hopp test notfound.json`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const cmd = `node ./bin/hopp invalid-arg`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
@@ -70,24 +80,24 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("ENV file not JSON type.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("ENV file not found.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
@@ -109,17 +119,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
|
||||
test("No value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Invalid value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
console.log("invalid value thing", out)
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { checkFile } from "../../../utils/checks";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("checkFile", () => {
|
||||
test("File doesn't exists.", () => {
|
||||
return expect(
|
||||
checkFile("./src/samples/this-file-not-exists.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
});
|
||||
});
|
||||
|
||||
test("File not of JSON type.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/notjson.txt")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "INVALID_FILE_TYPE",
|
||||
});
|
||||
});
|
||||
|
||||
test("Existing JSON file.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/passes.json")()
|
||||
).resolves.toBeRight();
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe("collectionsRunner", () => {
|
||||
|
||||
test("Empty HoppCollection.", () => {
|
||||
return expect(
|
||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
|
||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })
|
||||
).resolves.toStrictEqual([]);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([]);
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/request",
|
||||
@@ -116,7 +116,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/folder/request",
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { parseCollectionData } from "../../../utils/mutators";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("parseCollectionData", () => {
|
||||
test("Reading non-existing file.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/notexist.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
parseCollectionData("./src/__tests__/samples/notexist.json")
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
});
|
||||
});
|
||||
|
||||
test("Unparseable JSON contents.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/malformed-collection.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "MALFORMED_COLLECTION",
|
||||
parseCollectionData("./src/__tests__/samples/malformed-collection.json")
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "UNKNOWN_ERROR",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,15 +22,15 @@ describe("parseCollectionData", () => {
|
||||
return expect(
|
||||
parseCollectionData(
|
||||
"./src/__tests__/samples/malformed-collection2.json"
|
||||
)()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
)
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "MALFORMED_COLLECTION",
|
||||
});
|
||||
});
|
||||
|
||||
test("Valid HoppCollection.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/passes.json")()
|
||||
).resolves.toBeRight();
|
||||
parseCollectionData("./src/__tests__/samples/passes.json")
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { pipe, flow } from "fp-ts/function";
|
||||
import {
|
||||
collectionsRunner,
|
||||
collectionsRunnerExit,
|
||||
@@ -10,18 +8,23 @@ import { parseCollectionData } from "../utils/mutators";
|
||||
import { parseEnvsData } from "../options/test/env";
|
||||
import { TestCmdOptions } from "../types/commands";
|
||||
import { parseDelayOption } from "../options/test/delay";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { isHoppCLIError } from "../utils/checks";
|
||||
|
||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||
await pipe(
|
||||
TE.Do,
|
||||
TE.bind("envs", () => parseEnvsData(options.env)),
|
||||
TE.bind("collections", () => parseCollectionData(path)),
|
||||
TE.bind("delay", () => parseDelayOption(options.delay)),
|
||||
TE.chainTaskK(collectionsRunner),
|
||||
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
||||
TE.mapLeft((e) => {
|
||||
handleError(e);
|
||||
try {
|
||||
const delay = options.delay ? parseDelayOption(options.delay) : 0
|
||||
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
|
||||
const collections = await parseCollectionData(path)
|
||||
|
||||
const report = await collectionsRunner({collections, envs, delay})
|
||||
const hasSucceeded = collectionsRunnerResult(report)
|
||||
collectionsRunnerExit(hasSucceeded)
|
||||
} catch(e) {
|
||||
if(isHoppCLIError(e)) {
|
||||
handleError(e)
|
||||
process.exit(1);
|
||||
})
|
||||
)();
|
||||
}
|
||||
else throw e
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { log } from "console";
|
||||
import * as S from "fp-ts/string";
|
||||
import { HoppError, HoppErrorCode } from "../types/errors";
|
||||
import { hasProperty, isSafeCommanderError } from "../utils/checks";
|
||||
@@ -7,7 +6,7 @@ import { exceptionColors } from "../utils/getters";
|
||||
const { BG_FAIL } = exceptionColors;
|
||||
|
||||
/**
|
||||
* Parses unknown error data and narrows it to get information realted to
|
||||
* Parses unknown error data and narrows it to get information related to
|
||||
* error in string format.
|
||||
* @param e Error data to parse.
|
||||
* @returns Information in string format appropriately parsed, based on error type.
|
||||
@@ -81,6 +80,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
}
|
||||
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
log(ERROR_CODE, ERROR_MSG);
|
||||
console.error(ERROR_CODE, ERROR_MSG);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,20 +1,14 @@
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as S from "fp-ts/string";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { error, HoppCLIError } from "../../types/errors";
|
||||
import { error } from "../../types/errors";
|
||||
|
||||
export const parseDelayOption = (
|
||||
delay: unknown
|
||||
): TE.TaskEither<HoppCLIError, number> =>
|
||||
!S.isString(delay)
|
||||
? TE.right(0)
|
||||
: pipe(
|
||||
delay,
|
||||
Number,
|
||||
TE.fromPredicate(Number.isSafeInteger, () =>
|
||||
error({
|
||||
code: "INVALID_ARGUMENT",
|
||||
data: "Expected '-d, --delay' value to be number",
|
||||
})
|
||||
)
|
||||
);
|
||||
export function parseDelayOption(delay: string): number {
|
||||
const maybeInt = Number.parseInt(delay)
|
||||
|
||||
if(!Number.isNaN(maybeInt)) {
|
||||
return maybeInt
|
||||
} else {
|
||||
throw error({
|
||||
code: "INVALID_ARGUMENT",
|
||||
data: "Expected '-d, --delay' value to be number",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,27 @@
|
||||
import fs from "fs/promises";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as J from "fp-ts/Json";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import isArray from "lodash/isArray";
|
||||
import { HoppCLIError, error } from "../../types/errors";
|
||||
import { error } from "../../types/errors";
|
||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||
import { checkFile } from "../../utils/checks";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
export const parseEnvsData = (
|
||||
path: unknown
|
||||
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
|
||||
!S.isString(path)
|
||||
? TE.right({ global: [], selected: [] })
|
||||
: pipe(
|
||||
// Checking if the env.json file exists or not.
|
||||
checkFile(path),
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path)
|
||||
|
||||
// Trying to read given env json file path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) =>
|
||||
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
),
|
||||
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||
}
|
||||
|
||||
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
|
||||
TE.chainEitherKW((data) =>
|
||||
pipe(
|
||||
data.toString(),
|
||||
J.parse,
|
||||
E.map((jsonData) =>
|
||||
jsonData && typeof jsonData === "object" && !isArray(jsonData)
|
||||
? pipe(
|
||||
jsonData,
|
||||
Object.entries,
|
||||
A.map(
|
||||
([key, value]) =>
|
||||
<HoppEnvPair>{
|
||||
key,
|
||||
value: S.isString(value)
|
||||
? value
|
||||
: JSON.stringify(value),
|
||||
}
|
||||
)
|
||||
)
|
||||
: []
|
||||
),
|
||||
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
|
||||
E.mapLeft((e) =>
|
||||
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const envPairs: Array<HoppEnvPair> = []
|
||||
|
||||
for( const [key,value] of Object.entries(contents)) {
|
||||
if(typeof value !== "string") {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
||||
}
|
||||
|
||||
envPairs.push({key, value})
|
||||
}
|
||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type TestCmdOptions = {
|
||||
env: string;
|
||||
delay: number;
|
||||
env: string | undefined;
|
||||
delay: string | undefined;
|
||||
};
|
||||
|
||||
export type HoppEnvFileExt = "json";
|
||||
export type HOPP_ENV_FILE_EXT = "json";
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import fs from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
isHoppRESTRequest,
|
||||
} from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import curryRight from "lodash/curryRight";
|
||||
import { CommanderError } from "commander";
|
||||
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
import { HoppCollectionFileExt } from "../types/collections";
|
||||
import { HoppEnvFileExt } from "../types/commands";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
|
||||
/**
|
||||
* Determines whether an object has a property with given name.
|
||||
@@ -71,59 +62,6 @@ export const isRESTCollection = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the file path matches the requried file type with of required extension.
|
||||
* @param path The input file path to check.
|
||||
* @param extension The required extension for input file path.
|
||||
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFileExt = curryRight(
|
||||
(
|
||||
path: unknown,
|
||||
extension: HoppCollectionFileExt | HoppEnvFileExt
|
||||
): E.Either<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
|
||||
E.chainW(
|
||||
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
|
||||
error({ code: "INVALID_FILE_TYPE", data: extension })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if the given file path exists and is of given type.
|
||||
* @param path The input file path to check.
|
||||
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
|
||||
// Checking if path is string.
|
||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
||||
|
||||
/**
|
||||
* After checking file path, we map file path to absolute path and check
|
||||
* if file is of given extension type.
|
||||
*/
|
||||
TE.map(join),
|
||||
TE.chainEitherK(checkFileExt("json")),
|
||||
|
||||
/**
|
||||
* Trying to access given file path.
|
||||
* If successfully accessed, we return the path from predicate step.
|
||||
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
||||
*/
|
||||
TE.chainFirstW((checkedPath) =>
|
||||
TE.tryCatchK(
|
||||
() => fs.access(checkedPath),
|
||||
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
|
||||
)()
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if given error data is of type HoppCLIError, based on existence
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { bold } from "chalk";
|
||||
@@ -43,8 +42,8 @@ const { WARN, FAIL } = exceptionColors;
|
||||
* @returns List of report for each processed request.
|
||||
*/
|
||||
export const collectionsRunner =
|
||||
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
|
||||
async () => {
|
||||
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
|
||||
{
|
||||
const envs: HoppEnvs = param.envs;
|
||||
const delay = param.delay ?? 0;
|
||||
const requestsReport: RequestReport[] = [];
|
||||
@@ -213,10 +212,10 @@ export const collectionsRunnerResult = (
|
||||
* Else, exit with code 1.
|
||||
* @param result Boolean defining the collections-runner result.
|
||||
*/
|
||||
export const collectionsRunnerExit = (result: boolean) => {
|
||||
export const collectionsRunnerExit = (result: boolean): never => {
|
||||
if (!result) {
|
||||
const EXIT_MSG = FAIL(`\nExited with code 1`);
|
||||
process.stdout.write(EXIT_MSG);
|
||||
process.stderr.write(EXIT_MSG);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as J from "fp-ts/Json";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
|
||||
import { error } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
|
||||
/**
|
||||
@@ -39,49 +34,44 @@ export const parseErrorMessage = (e: unknown) => {
|
||||
return msg.replace(/\n+$|\s{2,}/g, "").trim();
|
||||
};
|
||||
|
||||
export async function readJsonFile(path: string): Promise<unknown> {
|
||||
if(!path.endsWith('.json')) {
|
||||
throw error({ code: "INVALID_FILE_TYPE", data: path })
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(path)
|
||||
} catch (e) {
|
||||
throw error({ code: "FILE_NOT_FOUND", path: path })
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse((await fs.readFile(path)).toString())
|
||||
} catch(e) {
|
||||
throw error({ code: "UNKNOWN_ERROR", data: e })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses collection json file for given path:context.path, and validates
|
||||
* the parsed collectiona array.
|
||||
* @param path Collection json file path.
|
||||
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||
*/
|
||||
export const parseCollectionData = (
|
||||
export async function parseCollectionData(
|
||||
path: string
|
||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
||||
pipe(
|
||||
TE.of(path),
|
||||
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||
let contents = await readJsonFile(path)
|
||||
|
||||
// Checking if given file path exists or not.
|
||||
TE.chain(checkFile),
|
||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
|
||||
|
||||
// Trying to read give collection json path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
),
|
||||
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
})
|
||||
}
|
||||
|
||||
// Checking if parsed file data is array.
|
||||
TE.chainEitherKW((data) =>
|
||||
pipe(
|
||||
data.toString(),
|
||||
J.parse,
|
||||
E.map((jsonData) => (Array.isArray(jsonData) ? jsonData : [jsonData])),
|
||||
E.mapLeft((e) =>
|
||||
error({ code: "MALFORMED_COLLECTION", path, data: E.toError(e) })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Validating collections to be HoppRESTCollection.
|
||||
TE.chainW(
|
||||
TE.fromPredicate(A.every(isRESTCollection), () =>
|
||||
error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||
};
|
||||
|
||||
@@ -36,6 +36,8 @@ module.exports = {
|
||||
"vue/no-side-effects-in-computed-properties": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"@typescript-eslint/no-unused-vars":
|
||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"import/default": "off",
|
||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
||||
* {
|
||||
@apply backface-hidden;
|
||||
@apply before: backface-hidden;
|
||||
@apply after: backface-hidden;
|
||||
@apply selection: bg-accentDark;
|
||||
@apply selection: text-accentContrast;
|
||||
@apply before:backface-hidden;
|
||||
@apply after:backface-hidden;
|
||||
@apply selection:bg-accentDark;
|
||||
@apply selection:text-accentContrast;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -21,8 +21,8 @@
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-solid border-transparent border-4;
|
||||
@apply hover: bg-dividerDark;
|
||||
@apply hover: bg-clip-content;
|
||||
@apply hover:bg-dividerDark;
|
||||
@apply hover:bg-clip-content;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -34,7 +34,7 @@ input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-35;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -91,6 +91,17 @@ body {
|
||||
@apply translate-x-full;
|
||||
}
|
||||
|
||||
.bounce-enter-active,
|
||||
.bounce-leave-active {
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
.bounce-enter-from,
|
||||
.bounce-leave-to {
|
||||
@apply transform;
|
||||
@apply scale-95;
|
||||
}
|
||||
|
||||
.svg-icons {
|
||||
@apply flex-shrink-0;
|
||||
@apply overflow-hidden;
|
||||
@@ -104,7 +115,7 @@ a {
|
||||
@apply no-underline;
|
||||
@apply transition;
|
||||
@apply leading-body;
|
||||
@apply focus: outline-none;
|
||||
@apply focus:outline-none;
|
||||
|
||||
&.link {
|
||||
@apply items-center;
|
||||
@@ -112,69 +123,94 @@ a {
|
||||
@apply -my-0.5 -mx-1;
|
||||
@apply text-accent;
|
||||
@apply rounded;
|
||||
@apply hover: text-accentDark;
|
||||
@apply focus-visible: ring;
|
||||
@apply focus-visible: ring-accent;
|
||||
@apply focus-visible: text-accentDark;
|
||||
@apply hover:text-accentDark;
|
||||
@apply focus-visible:ring;
|
||||
@apply focus-visible:ring-accent;
|
||||
@apply focus-visible:text-accentDark;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply fixed;
|
||||
@apply -mt-6;
|
||||
@apply inline-flex;
|
||||
@apply -mt-8;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme="tooltip"] {
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
@apply bg-tooltip;
|
||||
@apply text-primary;
|
||||
@apply font-semibold;
|
||||
@apply py-1 px-2;
|
||||
@apply border-solid border-tooltip;
|
||||
@apply rounded;
|
||||
@apply truncate;
|
||||
@apply shadow;
|
||||
@apply leading-body;
|
||||
font-size: 86%;
|
||||
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
@apply bg-gray-500/45;
|
||||
@apply text-primaryLight;
|
||||
@apply rounded-sm;
|
||||
@apply px-1;
|
||||
@apply my-0 ml-1;
|
||||
.tippy-content {
|
||||
@apply flex;
|
||||
@apply text-tiny text-primary;
|
||||
@apply font-semibold;
|
||||
@apply py-1 px-2;
|
||||
@apply truncate;
|
||||
@apply sm: inline-flex;
|
||||
@apply leading-normal;
|
||||
@apply items-center;
|
||||
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
@apply bg-gray-500/45;
|
||||
@apply text-primaryLight;
|
||||
@apply rounded-sm;
|
||||
@apply px-1;
|
||||
@apply my-0 ml-1;
|
||||
@apply truncate;
|
||||
@apply sm:inline-flex;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow svg {
|
||||
@apply fill-tooltip;
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme="popover"] {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-48;
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
.tippy-box[data-theme~="popover"] {
|
||||
@apply bg-popover;
|
||||
@apply text-secondary text-body;
|
||||
@apply p-2;
|
||||
@apply border-solid border-dividerDark;
|
||||
@apply rounded;
|
||||
@apply shadow-lg;
|
||||
@apply leading-body;
|
||||
@apply border border-dividerDark;
|
||||
@apply focus: outline-none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
.tippy-svg-arrow svg {
|
||||
@apply fill-popover;
|
||||
.tippy-content {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-56;
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@apply p-2;
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
@apply p-0;
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-dividerDark;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-popover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-v-tippy] {
|
||||
@@ -207,7 +243,7 @@ hr {
|
||||
@apply rounded;
|
||||
@apply text-secondaryDark;
|
||||
@apply border border-divider;
|
||||
@apply focus-visible: border-dividerDark;
|
||||
@apply focus-visible:border-dividerDark;
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -218,8 +254,8 @@ button {
|
||||
@apply transition;
|
||||
@apply text-body;
|
||||
@apply leading-body;
|
||||
@apply focus: outline-none;
|
||||
@apply disabled: cursor-not-allowed;
|
||||
@apply focus:outline-none;
|
||||
@apply disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.input[type="file"],
|
||||
@@ -250,6 +286,18 @@ button {
|
||||
@apply text-secondaryDark;
|
||||
}
|
||||
|
||||
.floating-input ~ .end-actions {
|
||||
@apply absolute;
|
||||
@apply right-0.2;
|
||||
@apply inset-y-0;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
.floating-input:has(~ .end-actions) {
|
||||
@apply pr-12;
|
||||
}
|
||||
|
||||
pre.ace_editor {
|
||||
@apply font-mono;
|
||||
@apply resize-none;
|
||||
@@ -268,16 +316,17 @@ pre.ace_editor {
|
||||
.select-wrapper {
|
||||
@apply flex flex-1;
|
||||
@apply relative;
|
||||
@apply after: absolute;
|
||||
@apply after: flex;
|
||||
@apply after: inset-y-0;
|
||||
@apply after: items-center;
|
||||
@apply after: justify-center;
|
||||
@apply after: pointer-events-none;
|
||||
@apply after: font-icon;
|
||||
@apply after: text-secondaryLight;
|
||||
@apply after: right-3;
|
||||
@apply after: content-["\e313"];
|
||||
@apply after:absolute;
|
||||
@apply after:flex;
|
||||
@apply after:inset-y-0;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:pointer-events-none;
|
||||
@apply after:font-icon;
|
||||
@apply after:text-current;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e313"];
|
||||
@apply after:text-lg;
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@@ -318,8 +367,8 @@ pre.ace_editor {
|
||||
@apply font-semibold;
|
||||
@apply transition;
|
||||
@apply leading-body;
|
||||
@apply sm: rounded;
|
||||
@apply sm: border;
|
||||
@apply sm:rounded;
|
||||
@apply sm:border;
|
||||
|
||||
.action {
|
||||
@apply relative;
|
||||
@@ -333,16 +382,16 @@ pre.ace_editor {
|
||||
@apply leading-body;
|
||||
@apply tracking-normal;
|
||||
@apply rounded;
|
||||
@apply last: ml-4;
|
||||
@apply sm: ml-8;
|
||||
@apply before: absolute;
|
||||
@apply before: bg-current;
|
||||
@apply before: opacity-10;
|
||||
@apply before: inset-0;
|
||||
@apply before: transition;
|
||||
@apply before: content-DEFAULT;
|
||||
@apply hover: no-underline;
|
||||
@apply hover: before:opacity-20;
|
||||
@apply last:ml-4;
|
||||
@apply sm:ml-8;
|
||||
@apply before:absolute;
|
||||
@apply before:bg-current;
|
||||
@apply before:opacity-10;
|
||||
@apply before:inset-0;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply hover:no-underline;
|
||||
@apply hover:before:opacity-20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,56 +417,46 @@ pre.ace_editor {
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
@apply before: absolute;
|
||||
@apply before: inset-0;
|
||||
@apply before: bg-accentLight;
|
||||
@apply before: opacity-0;
|
||||
@apply before: z-20;
|
||||
@apply before: transition;
|
||||
@apply before: content-DEFAULT;
|
||||
@apply after: absolute;
|
||||
@apply after: inset-0;
|
||||
@apply after: z-20;
|
||||
@apply after: transition;
|
||||
@apply after: flex;
|
||||
@apply after: items-center;
|
||||
@apply after: justify-center;
|
||||
@apply after: text-dividerDark;
|
||||
@apply after: font-icon;
|
||||
@apply hover: before:opacity-100;
|
||||
@apply hover: after:text-accentDark;
|
||||
@apply before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@apply before:opacity-0;
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply hover:before:opacity-100;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-1;
|
||||
@apply before: -left-0.5;
|
||||
@apply before: -right-0.5;
|
||||
@apply before: h-full;
|
||||
@apply after: content-["\e5d4"];
|
||||
@apply w-0;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-1;
|
||||
@apply before: -top-0.5;
|
||||
@apply before: -bottom-0.5;
|
||||
@apply before: w-full;
|
||||
@apply after: content-["\e5d3"];
|
||||
@apply h-0;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0.5;
|
||||
@apply w-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0.5;
|
||||
@apply h-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
@@ -453,13 +492,17 @@ pre.ace_editor {
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-1;
|
||||
@apply min-w-5;
|
||||
@apply min-h-5;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply border border-dividerDark;
|
||||
@apply shadow-sm;
|
||||
@apply <sm:hidden;
|
||||
}
|
||||
|
||||
.capitalize-first {
|
||||
@apply first-letter: capitalize;
|
||||
@apply first-letter:capitalize;
|
||||
}
|
||||
|
||||
details {
|
||||
@@ -470,6 +513,10 @@ details summary::-webkit-details-marker {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
details summary .indicator {
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
details[open] summary .indicator {
|
||||
@apply transform;
|
||||
@apply rotate-90;
|
||||
@@ -482,11 +529,33 @@ details[open] summary .indicator {
|
||||
}
|
||||
|
||||
.env-highlight {
|
||||
* {
|
||||
@apply text-accentContrast;
|
||||
@apply text-accentContrast;
|
||||
|
||||
&.env-found {
|
||||
@apply bg-accentDark;
|
||||
@apply hover:bg-accent;
|
||||
}
|
||||
|
||||
&.env-not-found {
|
||||
@apply bg-red-500;
|
||||
@apply hover:bg-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-accent #{!important};
|
||||
}
|
||||
|
||||
.color-picker[type="color"] {
|
||||
@apply appearance-none;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch-wrapper {
|
||||
@apply rounded;
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch {
|
||||
@apply rounded;
|
||||
@apply border-0;
|
||||
}
|
||||
@@ -6,16 +6,19 @@
|
||||
}
|
||||
|
||||
@mixin dark-theme {
|
||||
--primary-color: theme("colors.neutral.900");
|
||||
--primary-color: theme("colors.dark.800");
|
||||
--primary-light-color: theme("colors.dark.600");
|
||||
--primary-dark-color: theme("colors.neutral.800");
|
||||
--primary-contrast-color: #161616;
|
||||
--primary-contrast-color: theme("colors.neutral.900");
|
||||
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--secondary-dark-color: theme("colors.neutral.50");
|
||||
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
--divider-light-color: theme("colors.dark.500");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
|
||||
--error-color: theme("colors.stone.800");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.700");
|
||||
@@ -24,15 +27,18 @@
|
||||
|
||||
@mixin light-theme {
|
||||
--primary-color: theme("colors.white");
|
||||
--primary-light-color: theme("colors.neutral.50");
|
||||
--primary-dark-color: theme("colors.neutral.100");
|
||||
--primary-contrast-color: #fefefe;
|
||||
--secondary-color: theme("colors.neutral.500");
|
||||
--secondary-light-color: theme("colors.neutral.400");
|
||||
--secondary-dark-color: theme("colors.neutral.900");
|
||||
--primary-light-color: theme("colors.gray.50");
|
||||
--primary-dark-color: theme("colors.gray.100");
|
||||
--primary-contrast-color: theme("colors.light.50");
|
||||
|
||||
--secondary-color: theme("colors.gray.500");
|
||||
--secondary-light-color: theme("colors.gray.400");
|
||||
--secondary-dark-color: theme("colors.gray.900");
|
||||
|
||||
--divider-color: theme("colors.gray.100");
|
||||
--divider-light-color: theme("colors.neutral.100");
|
||||
--divider-dark-color: theme("colors.neutral.300");
|
||||
--divider-light-color: theme("colors.gray.100");
|
||||
--divider-dark-color: theme("colors.gray.300");
|
||||
|
||||
--error-color: theme("colors.yellow.100");
|
||||
--tooltip-color: theme("colors.neutral.800");
|
||||
--popover-color: theme("colors.white");
|
||||
@@ -43,16 +49,19 @@
|
||||
--primary-color: theme("colors.dark.900");
|
||||
--primary-light-color: theme("colors.neutral.900");
|
||||
--primary-dark-color: theme("colors.dark.800");
|
||||
--primary-contrast-color: #0e0e0e;
|
||||
--primary-contrast-color: theme("colors.dark.900");
|
||||
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
|
||||
--divider-color: theme("colors.dark.600");
|
||||
--divider-light-color: theme("colors.dark.800");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
--divider-dark-color: theme("colors.dark.200");
|
||||
|
||||
--error-color: theme("colors.stone.900");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.600");
|
||||
--popover-color: theme("colors.dark.900");
|
||||
--editor-theme: "twilight";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
overwrite: true
|
||||
schema: https://api.hoppscotch.io/graphql
|
||||
schema:
|
||||
- ${VITE_BACKEND_GQL_URL}
|
||||
generates:
|
||||
src/helpers/backend/graphql.ts:
|
||||
documents: "src/**/*.graphql"
|
||||
@@ -78,6 +78,12 @@
|
||||
"iso": "he-HE",
|
||||
"name": "עִברִית"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"file": "hi.json",
|
||||
"iso": "hi-HI",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "hu",
|
||||
"file": "hu.json",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "Maak alles skoon",
|
||||
"close": "Close",
|
||||
"connect": "Koppel",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Kopieer",
|
||||
"delete": "Vee uit",
|
||||
"disconnect": "Ontkoppel",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Redigeer",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "Filter",
|
||||
"go_back": "Gaan terug",
|
||||
"group_by": "Group by",
|
||||
"label": "Etiket",
|
||||
"learn_more": "Leer meer",
|
||||
"less": "Less",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "Soek",
|
||||
"send": "Stuur",
|
||||
"start": "Begin",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
"to_close": "to close",
|
||||
"to_navigate": "to navigate",
|
||||
@@ -168,10 +171,11 @@
|
||||
"members": "Span is leeg",
|
||||
"parameters": "Hierdie versoek het geen parameters nie",
|
||||
"pending_invites": "There are no pending invites for this team",
|
||||
"profile": "Login in to view your profile",
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokolle is leeg",
|
||||
"schema": "Koppel aan 'n GraphQL -eindpunt",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Spannaam leeg",
|
||||
"teams": "Spanne is leeg",
|
||||
"tests": "Daar is geen toetse vir hierdie versoek nie"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "Environment deletion",
|
||||
"edit": "Bewerk omgewing",
|
||||
"invalid_name": "Gee 'n geldige naam vir die omgewing",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "Nuwe omgewing",
|
||||
"no_environment": "Geen omgewing nie",
|
||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||
"select": "Kies omgewing",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Omgewings",
|
||||
"updated": "Environment updation",
|
||||
"variable_list": "Veranderlike lys"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "Dit lyk nie asof hierdie blaaier ondersteuning vir bedieners gestuurde geleenthede het nie.",
|
||||
"check_console_details": "Kyk na die konsole -log vir meer inligting.",
|
||||
"curl_invalid_format": "cURL is nie behoorlik geformateer nie",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Leë versoeknaam",
|
||||
"f12_details": "(F12 vir meer inligting)",
|
||||
"gql_prettify_invalid_query": "Kon nie 'n ongeldige navraag mooi maak nie, los sintaksisfoute op en probeer weer",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "Teken in met GitHub om 'n geheime idee te skep",
|
||||
"title": "Export"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Vouer geskep",
|
||||
"edit": "Wysig gids",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "Skema",
|
||||
"subscriptions": "Inskrywings"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Installeer toep",
|
||||
"login": "Teken aan",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "Invoer uitvoer"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "Kommunikasie",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Meld",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Boodskap",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "Publiseer",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "Teken in",
|
||||
"topic": "Onderwerp",
|
||||
"topic_name": "Onderwerpnaam",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "App Settings",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Aksent kleur",
|
||||
"account": "Rekening",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Pas u rekeninginstellings aan.",
|
||||
"account_email_description": "Jou primêre e -posadres.",
|
||||
"account_name_description": "Dit is u vertoonnaam.",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "Verander lettergrootte",
|
||||
"choose_language": "Kies taal",
|
||||
"dark_mode": "Donker",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Eksperimente",
|
||||
"experiments_notice": "Dit is 'n versameling eksperimente waaraan ons werk, wat nuttig, pret of beide kan wees. Hulle is nie finaal nie en is moontlik nie stabiel nie, dus moenie paniekerig raak as iets te vreemd gebeur nie. Skakel net die ding uit. Grappies eenkant,",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "test failed",
|
||||
"javascript_code": "JavaScript -kode",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "امسح الكل",
|
||||
"close": "Close",
|
||||
"connect": "الاتصال",
|
||||
"connecting": "Connecting",
|
||||
"copy": "نسخ",
|
||||
"delete": "حذف",
|
||||
"disconnect": "قطع الاتصال",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "كرر",
|
||||
"edit": "يحرر",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "Filter",
|
||||
"go_back": "عد",
|
||||
"group_by": "Group by",
|
||||
"label": "ملصق",
|
||||
"learn_more": "اقرأ أكثر",
|
||||
"less": "اقل",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "بحث",
|
||||
"send": "ارسل",
|
||||
"start": "ابدأ",
|
||||
"starting": "Starting",
|
||||
"stop": "قف",
|
||||
"to_close": "لإغلاء",
|
||||
"to_navigate": "للإنتقال",
|
||||
@@ -77,7 +80,7 @@
|
||||
"status": "حالة",
|
||||
"status_description": "Check the status of the website",
|
||||
"terms_and_privacy": "الشروط والخصوصية",
|
||||
"twitter": "تويتر",
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "اكتب أمرًا أو ابحث ...",
|
||||
"we_use_cookies": "نحن نستخدم ملفات تعريف الارتباط",
|
||||
"whats_new": "ما هو الجديد؟",
|
||||
@@ -172,6 +175,7 @@
|
||||
"protocols": "البروتوكولات فارغة",
|
||||
"schema": "اتصل بنقطة نهاية GraphQL",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "اسم الفريق فارغ",
|
||||
"teams": "الفرق فارغة",
|
||||
"tests": "لا توجد اختبارات لهذا الطلب"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "حذف بيئة العمل",
|
||||
"edit": "تحرير البيئة",
|
||||
"invalid_name": "الرجاء تقديم اسم صالح للبيئة",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "بيئة جديدة",
|
||||
"no_environment": "لا بيئة",
|
||||
"no_environment_description": "لم يتم اختيار أي بيئة عمل. اختر ما تريد فعله بالمتغيرات التالية.",
|
||||
"select": "حدد البيئة",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "البيئات",
|
||||
"updated": "تحديث بيئة العمل",
|
||||
"variable_list": "قائمة متغيرة"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "يبدو أن هذا المستعرض لا يدعم أحداث إرسال الخادم.",
|
||||
"check_console_details": "تحقق من سجل وحدة التحكم للحصول على التفاصيل.",
|
||||
"curl_invalid_format": "لم يتم تنسيق cURL بشكل صحيح",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "اسم الطلب فارغ",
|
||||
"f12_details": "(للحصول على تفاصيل F12)",
|
||||
"gql_prettify_invalid_query": "تعذر تحسين استعلام غير صالح وحل أخطاء بنية الاستعلام وحاول مرة أخرى",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "تسجيل الدخول باستخدام GitHub لإنشاء جوهر سري",
|
||||
"title": "Export"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "تم إنشاء المجلد",
|
||||
"edit": "تحرير المجلد",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "مخطط",
|
||||
"subscriptions": "الاشتراكات"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "تثبيت التطبيق",
|
||||
"login": "تسجيل الدخول",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "استيراد و تصدير"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "تواصل",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "سجل",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "رسالة",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "ينشر",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "يشترك",
|
||||
"topic": "عنوان",
|
||||
"topic_name": "اسم الموضوع",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "إعدادات التطبيق",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "محرر",
|
||||
"editor_description": "المحررين يمكنهم اضافة و تعديل و حذف الطلبات.",
|
||||
"email_verification_mail": "تم إرسال رابط التحقق إلى بريدك الإلكتروني. الرجاء الضغط على الرابط لتأكيد بريدك الإلكتروني.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "لون التمييز",
|
||||
"account": "حساب",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "تخصيص إعدادات حسابك.",
|
||||
"account_email_description": "عنوان بريدك الإلكتروني الأساسي.",
|
||||
"account_name_description": "هذا هو اسم العرض الخاص بك.",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "تغيير حجم الخط",
|
||||
"choose_language": "اختر اللغة",
|
||||
"dark_mode": "داكن",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "توسيع التنقل",
|
||||
"experiments": "التجارب",
|
||||
"experiments_notice": "هذه مجموعة من التجارب التي نعمل عليها والتي قد تكون مفيدة ، أو ممتعة ، أو كليهما ، أو لا شيء. إنها ليست نهائية وقد لا تكون مستقرة ، لذلك إذا حدث شيء غريب للغاية ، فلا داعي للذعر. فقط قم بإيقاف تشغيل الشيء. النكات جانبا،",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "لقد أرسلنا رابط دعوة لجميع المدعوين!",
|
||||
"we_sent_invite_link_description": "اطلب من جميع المدعوين التحقق من صندوق الوارد الخاص بهم. انقر على الرابط للانضمام إلى الفريق."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "فشل التجربة",
|
||||
"javascript_code": "كود جافا سكريبت",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "Neteja-ho tot",
|
||||
"close": "Close",
|
||||
"connect": "Connectar",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Copiar",
|
||||
"delete": "Eliminar",
|
||||
"disconnect": "Desconnectar",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"filter_response": "Filtrar resposta",
|
||||
"filter": "Filtrar resposta",
|
||||
"go_back": "Tornar",
|
||||
"group_by": "Group by",
|
||||
"label": "Etiquetar",
|
||||
"learn_more": "Aprèn més",
|
||||
"less": "Menys",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "Cercar",
|
||||
"send": "Enviar",
|
||||
"start": "Començar",
|
||||
"starting": "Starting",
|
||||
"stop": "Aturar",
|
||||
"to_close": "Tancar",
|
||||
"to_navigate": "Navegar",
|
||||
@@ -172,6 +175,7 @@
|
||||
"protocols": "Els protocols estan buits",
|
||||
"schema": "Connecta't a un endpoint GraphQL",
|
||||
"shortcodes": "Els shortcodes estan buits",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "El nom de l'equip és buit",
|
||||
"teams": "Els equips estan buits",
|
||||
"tests": "No hi ha proves per a aquesta sol·licitud"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "Entorn eliminat",
|
||||
"edit": "Editar l'entorn",
|
||||
"invalid_name": "Proporcioneu un nom vàlid per a l'entorn",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "Les variables d'entorn niuades estan limitades a 10 nivells",
|
||||
"new": "Nou entorn",
|
||||
"no_environment": "Sense entorn",
|
||||
"no_environment_description": "No s'ha seleccionat cap entorn. Trieu què voleu fer amb les variables següents.",
|
||||
"select": "Seleccioneu un entorn",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Entorns",
|
||||
"updated": "Entorn actualitzat",
|
||||
"variable_list": "Llista de variables"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "Sembla que aquest navegador no és compatible amb els Esdeveniments Enviats pel Servidor (Server Sent Events).",
|
||||
"check_console_details": "Consulta el registre de la consola per obtenir més informació.",
|
||||
"curl_invalid_format": "cURL no està formatat correctament",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Nom de la sol·licitud buida",
|
||||
"f12_details": "(F12 per obtenir més informació)",
|
||||
"gql_prettify_invalid_query": "No s'ha pogut definir una consulta no vàlida, resoldre els errors de sintaxi de la consulta i tornar-ho a provar",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "Inicieu la sessió amb GitHub per crear un Gisst secret",
|
||||
"title": "Exportar"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "S'ha creat la carpeta",
|
||||
"edit": "Editar la carpeta",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "Esquema",
|
||||
"subscriptions": "Subscripcions"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Instal·la l'aplicació",
|
||||
"login": "Iniciar Sessió",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "Importar / Exportar"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "Comunicació",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Registre",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Missatge",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "Publicar",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "subscriure's",
|
||||
"topic": "Tema",
|
||||
"topic_name": "Nom del Tema",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Configuració de l'aplicació",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Els editors poden afegir, editar i eliminar sol·licituds.",
|
||||
"email_verification_mail": "S'ha enviat un correu electrònic de verificació a la vostra adreça electrònica. Feu clic a l'enllaç per verificar la vostra adreça de correu electrònic.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Color d'accent",
|
||||
"account": "Compte",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Personalitzeu la configuració del compte.",
|
||||
"account_email_description": "La vostra adreça de correu electrònic principal.",
|
||||
"account_name_description": "Aquest és el vostre nom d'exposició",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "Canvia la mida de la lletra",
|
||||
"choose_language": "Tria l'idioma",
|
||||
"dark_mode": "Fosc",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Ampliar navegació",
|
||||
"experiments": "Experiments",
|
||||
"experiments_notice": "Es tracta d'una col·lecció d'experiments en què estem treballant que poden resultar útils, divertits, o ambdós. No són finals i potser no són estables, de manera que si passa alguna cosa massa estrany, no us espanteu. Només cal que el desactiveu. Bromes a part,",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "Hem enviat un enllaç d'invitació a tots els convidats!",
|
||||
"we_sent_invite_link_description": "Demaneu a tots els convidats que comprovin la seva safata d'entrada. Feu clic a l'enllaç per unir-vos a l'equip."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "prova fallada",
|
||||
"javascript_code": "Codi JavaScript",
|
||||
@@ -1,40 +1,43 @@
|
||||
{
|
||||
"action": {
|
||||
"autoscroll": "Autoscroll",
|
||||
"autoscroll": "自动滚动",
|
||||
"cancel": "取消",
|
||||
"choose_file": "选择文件",
|
||||
"clear": "清除",
|
||||
"clear_all": "全部清除",
|
||||
"close": "Close",
|
||||
"close": "关闭",
|
||||
"connect": "连接",
|
||||
"connecting": "连接中",
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"disconnect": "断开连接",
|
||||
"dismiss": "忽略",
|
||||
"dont_save": "不保存",
|
||||
"download_file": "下载文件",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"drag_to_reorder": "拖曳以重新排序",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "过滤",
|
||||
"go_back": "返回",
|
||||
"group_by": "分组方式",
|
||||
"label": "标签",
|
||||
"learn_more": "了解更多",
|
||||
"less": "更少",
|
||||
"more": "更多",
|
||||
"new": "新增",
|
||||
"no": "否",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "打开工作区",
|
||||
"paste": "粘贴",
|
||||
"prettify": "美化",
|
||||
"remove": "移除",
|
||||
"restore": "恢复",
|
||||
"save": "保存",
|
||||
"scroll_to_bottom": "Scroll to bottom",
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"scroll_to_bottom": "滚动至底部",
|
||||
"scroll_to_top": "滚动至顶部",
|
||||
"search": "搜索",
|
||||
"send": "发送",
|
||||
"start": "开始",
|
||||
"starting": "正在开始",
|
||||
"stop": "停止",
|
||||
"to_close": "以关闭",
|
||||
"to_navigate": "以定位",
|
||||
@@ -171,7 +174,8 @@
|
||||
"profile": "登录以查看你的个人档案",
|
||||
"protocols": "协议为空",
|
||||
"schema": "连接至 GraphQL 端点",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shortcodes": "Shortcodes 为空",
|
||||
"subscription": "订阅为空",
|
||||
"team_name": "团队名称为空",
|
||||
"teams": "团队为空",
|
||||
"tests": "没有针对该请求的测试"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "环境已删除",
|
||||
"edit": "编辑环境",
|
||||
"invalid_name": "请提供有效的环境名称",
|
||||
"my_environments": "我的环境",
|
||||
"nested_overflow": "环境嵌套深度超过限制(10层)",
|
||||
"new": "新建环境",
|
||||
"no_environment": "无环境",
|
||||
"no_environment_description": "没有选择环境。选择如何处理以下变量。",
|
||||
"select": "选择环境",
|
||||
"team_environments": "团队环境",
|
||||
"title": "环境",
|
||||
"updated": "环境已更新",
|
||||
"variable_list": "变量列表"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "该浏览器似乎不支持 SSE。",
|
||||
"check_console_details": "检查控制台日志以获悉详情",
|
||||
"curl_invalid_format": "cURL 格式不正确",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "您的帐号目前为这些团队的拥有者:",
|
||||
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
|
||||
"empty_req_name": "空请求名称",
|
||||
"f12_details": "(F12 详情)",
|
||||
"gql_prettify_invalid_query": "无法美化无效的查询,处理查询语法错误并重试",
|
||||
@@ -209,8 +218,8 @@
|
||||
"network_error": "好像发生了网络错误,请重试。",
|
||||
"network_fail": "无法发送请求",
|
||||
"no_duration": "无持续时间",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "找不到结果",
|
||||
"page_not_found": "找不到此頁面",
|
||||
"script_fail": "无法执行预请求脚本",
|
||||
"something_went_wrong": "发生了一些错误",
|
||||
"test_script_fail": "无法执行请求脚本"
|
||||
@@ -220,7 +229,12 @@
|
||||
"create_secret_gist": "创建私密 Gist",
|
||||
"gist_created": "已创建 Gist",
|
||||
"require_github": "使用 GitHub 登录以创建私密 Gist",
|
||||
"title": "Export"
|
||||
"title": "导出"
|
||||
},
|
||||
"filter": {
|
||||
"all": "全部",
|
||||
"none": "无",
|
||||
"starred": "已加星号"
|
||||
},
|
||||
"folder": {
|
||||
"created": "已创建文件夹",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "模式",
|
||||
"subscriptions": "订阅"
|
||||
},
|
||||
"group": {
|
||||
"time": "时间",
|
||||
"url": "网址"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "安装应用",
|
||||
"login": "登录",
|
||||
@@ -276,10 +294,10 @@
|
||||
"from_postman_description": "从 Postman 集合中导入",
|
||||
"from_url": "从 URL 导入",
|
||||
"gist_url": "输入 Gist URL",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"import_from_url_invalid_fetch": "无法从网址取得资料",
|
||||
"import_from_url_invalid_file_format": "导入组合时发生错误",
|
||||
"import_from_url_invalid_type": "不支持此类型。可接受的值为 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||
"import_from_url_success": "已导入组合",
|
||||
"json_description": "从 Hoppscotch 的集合文件导入(JSON)",
|
||||
"title": "导入"
|
||||
},
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "导入/导出"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "您已经订阅了此主題。",
|
||||
"clean_session": "清除会话",
|
||||
"clear_input": "清除输入",
|
||||
"clear_input_on_send": "发送后清除输入",
|
||||
"client_id": "客户端 ID",
|
||||
"color": "选择颜色",
|
||||
"communication": "通讯",
|
||||
"connection_config": "连接配置",
|
||||
"connection_not_authorized": "此MQTT连接未使用任何验证。",
|
||||
"invalid_topic": "请提供该订阅的主题",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "日志",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "消息",
|
||||
"new": "新订阅",
|
||||
"not_connected": "请先启动MQTT连接。",
|
||||
"publish": "发布",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "订阅",
|
||||
"topic": "主题",
|
||||
"topic_name": "主题名称",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "应用设置",
|
||||
"default_hopp_displayname": "未命名使用者",
|
||||
"editor": "编辑者",
|
||||
"editor_description": "编辑者可以添加、编辑和删除请求。",
|
||||
"email_verification_mail": "确认邮件已发送至你的邮箱,请点击链接以验证你的电子邮箱。",
|
||||
@@ -347,9 +384,9 @@
|
||||
"choose_language": "选择语言",
|
||||
"content_type": "内容类型",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "其他",
|
||||
"structured": "结构",
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "复制链接",
|
||||
"duration": "持续时间",
|
||||
@@ -381,11 +418,11 @@
|
||||
"type": "请求类型",
|
||||
"url": "URL",
|
||||
"variables": "变量",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "查看我的链接"
|
||||
},
|
||||
"response": {
|
||||
"body": "响应体",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "筛选JSON响应本体(使用JSONPath语法)",
|
||||
"headers": "响应头",
|
||||
"html": "HTML",
|
||||
"image": "图像",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "强调色",
|
||||
"account": "帐户",
|
||||
"account_deleted": "已刪除您的账号",
|
||||
"account_description": "自定义您的帐户设置。",
|
||||
"account_email_description": "您的主要电子邮箱地址。",
|
||||
"account_name_description": "这是您的显示名称。",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "更改字体大小",
|
||||
"choose_language": "选择语言",
|
||||
"dark_mode": "暗色",
|
||||
"delete_account": "刪除账号",
|
||||
"delete_account_description": "一旦您删除了您的帐号,您的所有数据将被永久删除。此操作无法复原。",
|
||||
"expand_navigation": "展开导航栏",
|
||||
"experiments": "实验功能",
|
||||
"experiments_notice": "下面是我们正在开发中的一些实验功能,这些功能可能会很有用,可能很有趣,又或者二者都是或都不是。这些功能并非最终版本且可能不稳定,所以如果发生了一些过于奇怪的事情,不要惊慌,关掉它们就好了。玩笑归玩笑,",
|
||||
@@ -437,8 +477,8 @@
|
||||
"proxy_use_toggle": "使用代理中间件发送请求",
|
||||
"read_the": "阅读",
|
||||
"reset_default": "重置为默认",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "快捷键",
|
||||
"short_codes_description": "我们为您打造的快捷键。",
|
||||
"sidebar_on_left": "侧边栏移至左侧",
|
||||
"sync": "同步",
|
||||
"sync_collections": "集合",
|
||||
@@ -452,16 +492,16 @@
|
||||
"theme_description": "自定义您的应用程序主题。",
|
||||
"use_experimental_url_bar": "使用实验性的带有环境高亮的 URL 栏",
|
||||
"user": "用户",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "已验证电子邮件地址",
|
||||
"verify_email": "验证电子邮箱"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"actions": "操作",
|
||||
"created_on": "创建于",
|
||||
"deleted": "已刪除快捷键",
|
||||
"method": "方法",
|
||||
"not_found": "找不到快捷键",
|
||||
"short_code": "快捷键",
|
||||
"url": "URL"
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -503,9 +543,9 @@
|
||||
"title": "请求"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "复制响应至剪贴板",
|
||||
"download": "下载响应",
|
||||
"title": "响应"
|
||||
},
|
||||
"theme": {
|
||||
"black": "切换为黑色主题",
|
||||
@@ -523,7 +563,7 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "通讯",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"connection_not_authorized": "此SocketIO连接未使用任何验证。",
|
||||
"event_name": "事件名称",
|
||||
"events": "事件",
|
||||
"log": "日志",
|
||||
@@ -541,9 +581,9 @@
|
||||
"connected": "已连接",
|
||||
"connected_to": "已连接到 {name}",
|
||||
"connecting_to": "正在连接到 {name}……",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_error": "连接错误",
|
||||
"connection_failed": "连接失败",
|
||||
"connection_lost": "连接丢失",
|
||||
"copied_to_clipboard": "已复制到剪贴板",
|
||||
"deleted": "已删除",
|
||||
"deprecated": "已弃用",
|
||||
@@ -558,17 +598,17 @@
|
||||
"history_deleted": "历史记录已删除",
|
||||
"linewrap": "换行",
|
||||
"loading": "正在加载……",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "信息:{message}已到达主题:{topic}",
|
||||
"mqtt_subscription_failed": "订阅此主题时发生错误:{topic}",
|
||||
"none": "无",
|
||||
"nothing_found": "没有找到",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"published_error": "将信息:{topic}发布至主题:{message}时发生错误",
|
||||
"published_message": "已将此信息:{message}发布至主题:{topic}",
|
||||
"reconnection_error": "重连失败",
|
||||
"subscribed_failed": "无法订阅此主題:{topic}",
|
||||
"subscribed_success": "成功订阅此主題:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主題:{topic}",
|
||||
"unsubscribed_success": "成功取消订阅此主題:{topic}",
|
||||
"waiting_send_request": "等待发送请求"
|
||||
},
|
||||
"support": {
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "我们向所有受邀者发送了邀请链接!",
|
||||
"we_sent_invite_link_description": "请所有受邀者检查他们的收件箱,点击链接以加入团队。"
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "已刪除环境",
|
||||
"duplicate": "已复制环境",
|
||||
"not_found": "找不到环境。"
|
||||
},
|
||||
"test": {
|
||||
"failed": "测试失败",
|
||||
"javascript_code": "JavaScript 代码",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "Vymazat vše",
|
||||
"close": "Close",
|
||||
"connect": "Připojit",
|
||||
"connecting": "Connecting",
|
||||
"copy": "kopírovat",
|
||||
"delete": "Vymazat",
|
||||
"disconnect": "Odpojit",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Upravit",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "Filter",
|
||||
"go_back": "Vrať se",
|
||||
"group_by": "Group by",
|
||||
"label": "Označení",
|
||||
"learn_more": "Další informace",
|
||||
"less": "Less",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "Vyhledávání",
|
||||
"send": "Poslat",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
"to_close": "to close",
|
||||
"to_navigate": "to navigate",
|
||||
@@ -77,7 +80,7 @@
|
||||
"status": "Postavení",
|
||||
"status_description": "Check the status of the website",
|
||||
"terms_and_privacy": "Podmínky a soukromí",
|
||||
"twitter": "Cvrlikání",
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "Zadejte příkaz nebo hledejte…",
|
||||
"we_use_cookies": "Používáme cookies",
|
||||
"whats_new": "Co je nového?",
|
||||
@@ -168,10 +171,11 @@
|
||||
"members": "Tým je prázdný",
|
||||
"parameters": "Tento požadavek nemá žádné parametry",
|
||||
"pending_invites": "There are no pending invites for this team",
|
||||
"profile": "Login in to view your profile",
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokoly jsou prázdné",
|
||||
"schema": "Připojte se ke koncovému bodu GraphQL",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Název týmu prázdný",
|
||||
"teams": "Týmy jsou prázdné",
|
||||
"tests": "Pro tento požadavek neexistují žádné testy"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "Environment deletion",
|
||||
"edit": "Upravit prostředí",
|
||||
"invalid_name": "Zadejte platný název prostředí",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "Nové prostředí",
|
||||
"no_environment": "Žádné prostředí",
|
||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||
"select": "Vyberte prostředí",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Prostředí",
|
||||
"updated": "Environment updation",
|
||||
"variable_list": "Seznam proměnných"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "Zdá se, že tento prohlížeč nemá podporu událostí odeslaných serverem.",
|
||||
"check_console_details": "Podrobnosti najdete v protokolu konzoly.",
|
||||
"curl_invalid_format": "cURL nemá správný formát",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Název prázdného požadavku",
|
||||
"f12_details": "(F12 pro podrobnosti)",
|
||||
"gql_prettify_invalid_query": "Neplatný dotaz nelze předběžně upravit, vyřešit chyby syntaxe dotazu a zkusit to znovu",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "Přihlaste se pomocí GitHub a vytvořte tajný seznam",
|
||||
"title": "Export"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Složka vytvořena",
|
||||
"edit": "Upravit složku",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "Schéma",
|
||||
"subscriptions": "Předplatné"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Nainstalovat aplikaci",
|
||||
"login": "Přihlásit se",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "Import Export"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "Sdělení",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Záznam",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Zpráva",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "Publikovat",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "předplatit",
|
||||
"topic": "Téma",
|
||||
"topic_name": "Název tématu",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "App Settings",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Akcentní barva",
|
||||
"account": "Účet",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Přizpůsobte si nastavení účtu.",
|
||||
"account_email_description": "Vaše primární e -mailová adresa.",
|
||||
"account_name_description": "Toto je vaše zobrazované jméno.",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "Změnit velikost písma",
|
||||
"choose_language": "Vyber jazyk",
|
||||
"dark_mode": "Temný",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Experimenty",
|
||||
"experiments_notice": "Toto je sbírka experimentů, na kterých pracujeme a které se mohou ukázat jako užitečné, zábavné, obojí, nebo ani jedno. Nejsou konečné a nemusí být stabilní, takže pokud se stane něco příliš divného, nepanikařte. Prostě vypni tu nebezpečnou věc. Vtipy stranou,",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "test failed",
|
||||
"javascript_code": "JavaScriptový kód",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "Slet alt",
|
||||
"close": "Close",
|
||||
"connect": "Opret forbindelse",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Kopi",
|
||||
"delete": "Slet",
|
||||
"disconnect": "Koble fra",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Redigere",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "Filter",
|
||||
"go_back": "Gå tilbage",
|
||||
"group_by": "Group by",
|
||||
"label": "Etiket",
|
||||
"learn_more": "Lær mere",
|
||||
"less": "Less",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "Søg",
|
||||
"send": "Sende",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Hold op",
|
||||
"to_close": "to close",
|
||||
"to_navigate": "to navigate",
|
||||
@@ -168,10 +171,11 @@
|
||||
"members": "Holdet er tomt",
|
||||
"parameters": "Denne anmodning har ingen parametre",
|
||||
"pending_invites": "There are no pending invites for this team",
|
||||
"profile": "Login in to view your profile",
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokoller er tomme",
|
||||
"schema": "Opret forbindelse til et GraphQL -slutpunkt",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Teamnavn er tomt",
|
||||
"teams": "Hold er tomme",
|
||||
"tests": "Der er ingen test for denne anmodning"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "Environment deletion",
|
||||
"edit": "Rediger miljø",
|
||||
"invalid_name": "Angiv et gyldigt navn på miljøet",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "Nyt miljø",
|
||||
"no_environment": "Intet miljø",
|
||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||
"select": "Vælg miljø",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Miljøer",
|
||||
"updated": "Environment updation",
|
||||
"variable_list": "Variabel liste"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "Det ser ikke ud til, at denne browser understøtter Server Sent Events.",
|
||||
"check_console_details": "Tjek konsollog for at få flere oplysninger.",
|
||||
"curl_invalid_format": "cURL er ikke formateret korrekt",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Tom anmodningsnavn",
|
||||
"f12_details": "(F12 for detaljer)",
|
||||
"gql_prettify_invalid_query": "Kunne ikke prætificere en ugyldig forespørgsel, løse forespørgselssyntaksfejl og prøve igen",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "Log ind med GitHub for at skabe hemmelig kerne",
|
||||
"title": "Export"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Mappe oprettet",
|
||||
"edit": "Rediger mappe",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "Skema",
|
||||
"subscriptions": "Abonnementer"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Installer app",
|
||||
"login": "Log på",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "Import Eksport"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "Meddelelse",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Log",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Besked",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "Offentliggøre",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "Abonner",
|
||||
"topic": "Emne",
|
||||
"topic_name": "Emne navn",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "App Settings",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Accent farve",
|
||||
"account": "Konto",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Tilpas dine kontoindstillinger.",
|
||||
"account_email_description": "Din primære e -mail -adresse.",
|
||||
"account_name_description": "Dette er dit visningsnavn.",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "Skift skriftstørrelse",
|
||||
"choose_language": "Vælg sprog",
|
||||
"dark_mode": "Mørk",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Eksperimenter",
|
||||
"experiments_notice": "Dette er en samling af eksperimenter, vi arbejder på, der kan vise sig at være nyttige, sjove, begge dele eller ingen af dem. De er ikke endelige og er muligvis ikke stabile, så hvis der sker noget alt for mærkeligt, skal du ikke gå i panik. Bare sluk for dang -tingen. Vittigheder til side,",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "test failed",
|
||||
"javascript_code": "JavaScript -kode",
|
||||
@@ -7,6 +7,7 @@
|
||||
"clear_all": "Alles zurücksetzen",
|
||||
"close": "Close",
|
||||
"connect": "Verbinden",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Kopieren",
|
||||
"delete": "Löschen",
|
||||
"disconnect": "Trennen",
|
||||
@@ -16,8 +17,9 @@
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"duplicate": "Duplizieren",
|
||||
"edit": "Bearbeiten",
|
||||
"filter_response": "Filter response",
|
||||
"filter": "Filter",
|
||||
"go_back": "Zurück",
|
||||
"group_by": "Group by",
|
||||
"label": "Etikett",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"less": "Weniger",
|
||||
@@ -35,6 +37,7 @@
|
||||
"search": "Suchen",
|
||||
"send": "Senden",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stopp",
|
||||
"to_close": "zum Schließen",
|
||||
"to_navigate": "zum Navigieren",
|
||||
@@ -172,6 +175,7 @@
|
||||
"protocols": "Protokolle sind leer",
|
||||
"schema": "Verbinden mit einem GraphQL-Endpunkt",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Teamname leer",
|
||||
"teams": "Teams sind leer",
|
||||
"tests": "Es gibt keine Tests für diese Anfrage"
|
||||
@@ -184,11 +188,13 @@
|
||||
"deleted": "Umgebung löschen",
|
||||
"edit": "Umgebung bearbeiten",
|
||||
"invalid_name": "Bitte gib einen gültigen Namen für die Umgebung an",
|
||||
"my_environments": "My Environments",
|
||||
"nested_overflow": "Verschachtelte Umgebungsvariablen sind limitert auf 10 Unterebenen",
|
||||
"new": "Neue Umgebung",
|
||||
"no_environment": "Keine Umgebung",
|
||||
"no_environment_description": "Es wurden keine Umgebungen ausgewählt. Wähle aus, was mit den untenstehenden Variablen geschehen soll.",
|
||||
"select": "Umgebung auswählen",
|
||||
"team_environments": "Team Environments",
|
||||
"title": "Umgebungen",
|
||||
"updated": "Umgebung aktualisiert",
|
||||
"variable_list": "Variablenliste"
|
||||
@@ -197,6 +203,9 @@
|
||||
"browser_support_sse": "Dieser Browser scheint keine Unterstützung für die vom Server gesendete Ereignisse zu haben.",
|
||||
"check_console_details": "Einzelheiten findest Du in der Browser-Konsole.",
|
||||
"curl_invalid_format": "cURL ist nicht richtig formatiert",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Leerer Anfragename",
|
||||
"f12_details": "(F12 für Details)",
|
||||
"gql_prettify_invalid_query": "Eine ungültige Abfrage konnte nicht verschönert werden. Fehler in der Abfragesyntax beheben und erneut versuchen",
|
||||
@@ -222,6 +231,11 @@
|
||||
"require_github": "Melde Dich bei GitHub an, um einen geheimen Gist zu erstellen",
|
||||
"title": "Export"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Ordner erstellt",
|
||||
"edit": "Ordner bearbeiten",
|
||||
@@ -235,6 +249,10 @@
|
||||
"schema": "Schema",
|
||||
"subscriptions": "Abonnements"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "App installieren",
|
||||
"login": "Anmeldung",
|
||||
@@ -298,10 +316,28 @@
|
||||
"import_export": "Import / Export"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"communication": "Kommunikation",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Protokoll",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Nachricht",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"publish": "Veröffentlichen",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
"subscribe": "Abonnieren",
|
||||
"topic": "Thema",
|
||||
"topic_name": "Themenname",
|
||||
@@ -325,6 +361,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "App-Einstellungen",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Editoren können neue Anfragen hinzufügen, bearbeiten und löschen.",
|
||||
"email_verification_mail": "Eine Bestätigungsmail wurde an Deine primäre E-Mail-Adresse versendet. Bitte klicke auf den darin stehenden Link um Deine E-Mail-Adresse zu bestätigen.",
|
||||
@@ -403,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Akzentfarbe",
|
||||
"account": "Konto",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Passe Deine Kontoeinstellungen an.",
|
||||
"account_email_description": "Deine primäre E-Mail-Adresse.",
|
||||
"account_name_description": "Dies ist Dein Anzeigename.",
|
||||
@@ -411,6 +449,8 @@
|
||||
"change_font_size": "Schriftgröße ändern",
|
||||
"choose_language": "Sprache wählen",
|
||||
"dark_mode": "Dunkel",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Menüpunkte vergrößern",
|
||||
"experiments": "Experimente",
|
||||
"experiments_notice": "Dies ist eine Sammlung von Experimenten, an denen wir aktuell arbeiten und die sich als nützlich erweisen könnten, Spaß machen, beides oder keines von beiden. Sie sind nicht endgültig und möglicherweise nicht stabil. Wenn also etwas übermäßig Seltsames passiert, gerate nicht in Panik. Schalte das verdammte Ding einfach aus. Scherz beiseite,",
|
||||
@@ -652,6 +692,11 @@
|
||||
"we_sent_invite_link": "Einladungen wurden an alle E-Mails verschickt!",
|
||||
"we_sent_invite_link_description": "Bitte alle eingeladenen Personen, ihren Posteingang zu überprüfen. Klicke auf den Link, um dem Team beizutreten."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"failed": "Test fehlgeschlagen",
|
||||
"javascript_code": "JavaScript-Code",
|
||||