Compare commits
320 Commits
refactor/t
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3a1898dad | ||
|
|
45e508fc36 | ||
|
|
8edad7ded7 | ||
|
|
75e1adb7b3 | ||
|
|
22ac13f2f0 | ||
|
|
7f0246eb47 | ||
|
|
5f0800760f | ||
|
|
6db99c9e37 | ||
|
|
3b2cabd3f3 | ||
|
|
b0dd6b0bd6 | ||
|
|
874b846e60 | ||
|
|
dbe2525c6f | ||
|
|
afd414fa3f | ||
|
|
94763dcb31 | ||
|
|
6314740f46 | ||
|
|
d7332120e3 | ||
|
|
8c74fe9925 | ||
|
|
c74ddeb530 | ||
|
|
e31c0a9d02 | ||
|
|
d9e5d4aec5 | ||
|
|
c1ee8f5dd0 | ||
|
|
dd59de3de0 | ||
|
|
511a3c55f3 | ||
|
|
c9021ab3ca | ||
|
|
a765c4a7cc | ||
|
|
ea99732474 | ||
|
|
6c64ffe833 | ||
|
|
5fa6c6cdb3 | ||
|
|
d94759870e | ||
|
|
f0a6fc641a | ||
|
|
7ba00bee0b | ||
|
|
dc2bdf81b9 | ||
|
|
187a30abac | ||
|
|
5b824ccb17 | ||
|
|
3bdf2baf97 | ||
|
|
9af8a24a89 | ||
|
|
57c4759bdb | ||
|
|
d9d7261bc5 | ||
|
|
a12315d81a | ||
|
|
9f0956556f | ||
|
|
748318d44e | ||
|
|
ff3062cdfc | ||
|
|
48d67fe7e1 | ||
|
|
2c9918f9a7 | ||
|
|
ee03952201 | ||
|
|
43dcd3c443 | ||
|
|
8f8c42a92a | ||
|
|
6496aded25 | ||
|
|
eacf8113af | ||
|
|
c4a1527153 | ||
|
|
4a89a6aafc | ||
|
|
52539b084d | ||
|
|
8d5bd051a1 | ||
|
|
3809e9853e | ||
|
|
5f795acd61 | ||
|
|
17c550404f | ||
|
|
a840079119 | ||
|
|
2761894164 | ||
|
|
6b8bc618dc | ||
|
|
258f79604f | ||
|
|
81ae70ee04 | ||
|
|
6b02d290a5 | ||
|
|
7ab1bbaf62 | ||
|
|
079083d0f2 | ||
|
|
0504707aab | ||
|
|
fb4aab875d | ||
|
|
7bb32ecf7e | ||
|
|
e129a5c179 | ||
|
|
8045f26c19 | ||
|
|
86516421b5 | ||
|
|
bce88ccd44 | ||
|
|
66d408b7db | ||
|
|
297bf3205f | ||
|
|
7366b32349 | ||
|
|
b7e0169c9b | ||
|
|
6b6f85cc7e | ||
|
|
2c014a2f4b | ||
|
|
d6df675821 | ||
|
|
427baf4c79 | ||
|
|
4f2b682341 | ||
|
|
e03f888cb2 | ||
|
|
513396d498 | ||
|
|
8f04f0758b | ||
|
|
7a77bfc248 | ||
|
|
ddd29374ea | ||
|
|
4b0d7a6c3d | ||
|
|
20bfc02a4e | ||
|
|
2511724b73 | ||
|
|
a851ee3fab | ||
|
|
40f6e6f8a8 | ||
|
|
e2fd104c2d | ||
|
|
a3eafa54fa | ||
|
|
b90b4a1910 | ||
|
|
247df4d5b9 | ||
|
|
4a3889a76e | ||
|
|
224a6e069c | ||
|
|
2c3097eeb7 | ||
|
|
3289ede0e8 | ||
|
|
ddf21c17d2 | ||
|
|
c9a24a0d28 | ||
|
|
fa4b130b18 | ||
|
|
8561a7547f | ||
|
|
e85f7b8232 | ||
|
|
2f91d25ed4 | ||
|
|
ae304b5af7 | ||
|
|
1c51f8b32e | ||
|
|
aaff07bba2 | ||
|
|
4bc38d5e0f | ||
|
|
fdfca00886 | ||
|
|
d872e393f8 | ||
|
|
686d8e5be7 | ||
|
|
4e30efd737 | ||
|
|
aa4935c505 | ||
|
|
0e381ab850 | ||
|
|
4a03ee4518 | ||
|
|
be414d8279 | ||
|
|
c47b1f2413 | ||
|
|
ba9ee052a6 | ||
|
|
b1c6708762 | ||
|
|
0ba31b6c79 | ||
|
|
14f402f186 | ||
|
|
b811e97ea5 | ||
|
|
6d167ce1d6 | ||
|
|
9ac1d23fd9 | ||
|
|
dea3a34e3d | ||
|
|
39de34f083 | ||
|
|
02d5f0fdf3 | ||
|
|
0bb7cbe8d9 | ||
|
|
3e3b88b8c2 | ||
|
|
1438beb93b | ||
|
|
c1ec5dc60d | ||
|
|
3f513f2f4d | ||
|
|
d1b573f6f9 | ||
|
|
0e08abc46f | ||
|
|
49bdf9f203 | ||
|
|
b3e9df4f3d | ||
|
|
3b8cf4a60a | ||
|
|
b7ccb9a34c | ||
|
|
b8ffa872c7 | ||
|
|
ef866f7851 | ||
|
|
f27515bf1d | ||
|
|
ddf74c5d7c | ||
|
|
8ea12695b3 | ||
|
|
d6324e6ba6 | ||
|
|
2325982801 | ||
|
|
b103c45e65 | ||
|
|
409989eddb | ||
|
|
3f5fcae280 | ||
|
|
d5123c793a | ||
|
|
e6707c1e4a | ||
|
|
41be5cc4a8 | ||
|
|
e82a4a1d23 | ||
|
|
e30e4edfce | ||
|
|
bd72ef7950 | ||
|
|
539034df2a | ||
|
|
6da6afc5a1 | ||
|
|
fea523972d | ||
|
|
5cfc6c2949 | ||
|
|
6dd0c25d49 | ||
|
|
ab9b3e47b9 | ||
|
|
33ebdf2831 | ||
|
|
ef95939763 | ||
|
|
0394deaeef | ||
|
|
337a60c8a4 | ||
|
|
47b341d50e | ||
|
|
a5fd39adf8 | ||
|
|
5772117dc8 | ||
|
|
f73c8a45d9 | ||
|
|
109d4190ae | ||
|
|
317de82be6 | ||
|
|
3604d69463 | ||
|
|
96cf774652 | ||
|
|
d2b39976ba | ||
|
|
06161bc963 | ||
|
|
2ab1d3dbfa | ||
|
|
ecdc7919ae | ||
|
|
a24541ac2b | ||
|
|
d832690548 | ||
|
|
2844710ea8 | ||
|
|
84ad4071ad | ||
|
|
7f501241f0 | ||
|
|
5dcfa66c5d | ||
|
|
f428a21279 | ||
|
|
ccdd4963cd | ||
|
|
2092a3729c | ||
|
|
a628420adb | ||
|
|
23de147ca1 | ||
|
|
93f55f5619 | ||
|
|
b10933898f | ||
|
|
1727b754d4 | ||
|
|
ffb0c12c08 | ||
|
|
c332808fe4 | ||
|
|
8f0538c886 | ||
|
|
e6bb7e2ca9 | ||
|
|
2a012520d0 | ||
|
|
c71333d9cb | ||
|
|
728515c225 | ||
|
|
e0f88e01f9 | ||
|
|
1aa94a12c0 | ||
|
|
de2d3361a7 | ||
|
|
680937e50b | ||
|
|
6751c50514 | ||
|
|
0c389701fe | ||
|
|
9454d8c100 | ||
|
|
166f9e817b | ||
|
|
d2865c637c | ||
|
|
9698932bde | ||
|
|
44026fcd41 | ||
|
|
d938af0c2c | ||
|
|
fd658400a6 | ||
|
|
dcbb17b164 | ||
|
|
49741875bd | ||
|
|
0fcd9733ff | ||
|
|
62d50169d7 | ||
|
|
1d3d5a1e6a | ||
|
|
d309fa745e | ||
|
|
f7031992d5 | ||
|
|
fcdf68ea15 | ||
|
|
b0a6692179 | ||
|
|
e1e763575d | ||
|
|
4236d1179c | ||
|
|
09365bcabe | ||
|
|
be29ddcbd6 | ||
|
|
522194ca8d | ||
|
|
5af8f584f6 | ||
|
|
adc08f8865 | ||
|
|
0f39d54c3c | ||
|
|
9e6659e842 | ||
|
|
46a0f6e3f8 | ||
|
|
e90b26ebed | ||
|
|
4407f260ae | ||
|
|
d4392416c8 | ||
|
|
2d3cbd26b8 | ||
|
|
98b9660956 | ||
|
|
4e8a4e8914 | ||
|
|
96bcbc80f8 | ||
|
|
1dfc8e2973 | ||
|
|
311886f6c9 | ||
|
|
4a332f40e5 | ||
|
|
93a97a2f4c | ||
|
|
1dee098ca2 | ||
|
|
a07cc7e560 | ||
|
|
c26f7f5ebc | ||
|
|
5d801cf566 | ||
|
|
631b2d869e | ||
|
|
c02f54cc18 | ||
|
|
827a95515d | ||
|
|
9082152f1a | ||
|
|
0efbddeda4 | ||
|
|
b2e186957c | ||
|
|
d855e5cffb | ||
|
|
f3747edaa3 | ||
|
|
752932ef3d | ||
|
|
948cf9dae3 | ||
|
|
b2f93aa549 | ||
|
|
108f228edf | ||
|
|
fe6030140f | ||
|
|
003400cfa8 | ||
|
|
41a02f059d | ||
|
|
b4ed6fd107 | ||
|
|
36246da9e1 | ||
|
|
457b6b982c | ||
|
|
05a07dc4a1 | ||
|
|
85889c2cb9 | ||
|
|
be6ceaab04 | ||
|
|
f1b18688bb | ||
|
|
80c7decb81 | ||
|
|
3ef5a1e21a | ||
|
|
2eb0a4c754 | ||
|
|
10f5af5dda | ||
|
|
8b27ebb96b | ||
|
|
b28f82a881 | ||
|
|
c921606f3f | ||
|
|
c6c08f6c60 | ||
|
|
02cf620090 | ||
|
|
4a12cc76fa | ||
|
|
f4f74e223f | ||
|
|
8b4535c131 | ||
|
|
b15fd6c75a | ||
|
|
e1a25fa894 | ||
|
|
2bb3b71a70 | ||
|
|
4c55b9c304 | ||
|
|
639a629809 | ||
|
|
d6e3bd09b4 | ||
|
|
8d67a0d95f | ||
|
|
b9fc0175e7 | ||
|
|
dc5f52cc0d | ||
|
|
602aabdeb8 | ||
|
|
2f8aa79ec1 | ||
|
|
8af90432cf | ||
|
|
61da0733c2 | ||
|
|
33951482d5 | ||
|
|
4e8484ee7c | ||
|
|
071761a61e | ||
|
|
10a11d6725 | ||
|
|
c81178ae26 | ||
|
|
2bafae5397 | ||
|
|
6a1d201e0e | ||
|
|
8de544696d | ||
|
|
66c489da8f | ||
|
|
26c8f35688 | ||
|
|
28aeac4533 | ||
|
|
162b3d6192 | ||
|
|
b016d3fd9d | ||
|
|
f64ff58dbc | ||
|
|
d4d3d96bbb | ||
|
|
a5197ee544 | ||
|
|
8a5fd4f745 | ||
|
|
12cd7940c6 | ||
|
|
0c2cec46a7 | ||
|
|
8430921e4e | ||
|
|
c938abf606 | ||
|
|
3addfe8d4b | ||
|
|
5276556837 | ||
|
|
e47ad94666 | ||
|
|
7065763c7c | ||
|
|
86489d95c2 | ||
|
|
e2b1c83698 | ||
|
|
15373be63e | ||
|
|
8c9cd079b7 |
13
.github/workflows/tests.yml
vendored
@@ -2,13 +2,12 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -17,9 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Install pnpm
|
||||
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
cache: pnpm
|
||||
- name: Run tests
|
||||
run: pnpm i && pnpm -r test
|
||||
|
||||
6
.gitignore
vendored
@@ -104,3 +104,9 @@ tests/*/screenshots
|
||||
|
||||
# Tests videos
|
||||
tests/*/videos
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# Andrew's crazy Volar shim generator
|
||||
shims-volar.d.ts
|
||||
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
16
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:12-alpine
|
||||
FROM node:lts-alpine
|
||||
|
||||
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
|
||||
|
||||
@@ -9,17 +9,19 @@ RUN apk add --update --no-cache \
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
ADD . /app/
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN pnpm i --unsafe-perm=true
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
EXPOSE 3000
|
||||
|
||||
RUN mv .env.example .env
|
||||
RUN mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
RUN pnpm run generate
|
||||
|
||||
CMD ["pnpm", "run", "start"]
|
||||
|
||||
29
README.md
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<a href="https://hoppscotch.io">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/hoppscotch/hoppscotch/main/static/logo.png"
|
||||
src="https://avatars.githubusercontent.com/u/56705483"
|
||||
alt="Hoppscotch Logo"
|
||||
height="64"
|
||||
/>
|
||||
@@ -89,7 +89,7 @@
|
||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
||||
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||
|
||||
🌈 **Make it yours:** Customizable combinations for background, foreground and accent colors — [customize now ✨](https://hoppscotch.io/settings).
|
||||
🌈 **Make it yours:** Customizable combinations for background, foreground and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||
|
||||
**Theming**
|
||||
|
||||
@@ -173,7 +173,7 @@ _Collections are synced with cloud / local session storage_
|
||||
|
||||
- Hide your IP address
|
||||
- Fixes [`CORS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross Origin Resource Sharing) issues
|
||||
- Access APIs served in non-HTTPS `[http://]` endpoints
|
||||
- Access APIs served in non-HTTPS (`http://`) endpoints
|
||||
- Use your own Proxy URL
|
||||
|
||||
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/privacy)**_
|
||||
@@ -256,9 +256,12 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
|
||||
👨👩👧👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
|
||||
|
||||
- Unlimited team collections and shared requests
|
||||
- Unlimited teams
|
||||
- Unlimited shared collections
|
||||
- Unlimited team members
|
||||
- User roles
|
||||
- Role-based access control
|
||||
- Cloud sync
|
||||
- Multiple devices
|
||||
|
||||
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||
|
||||
@@ -290,7 +293,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
|
||||
## **Developing**
|
||||
|
||||
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in repository's root directory with your own keys and rename it to `.env`.
|
||||
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`.
|
||||
|
||||
_Sample keys only works with the [production build](https://hoppscotch.io)._
|
||||
|
||||
@@ -302,9 +305,10 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
|
||||
### Local development environment
|
||||
|
||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
||||
2. Install dependencies by running `npm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
3. Start the development server with `npm run dev`.
|
||||
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
||||
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. Start the development server with `pnpm run dev`.
|
||||
5. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
||||
|
||||
### Docker compose
|
||||
|
||||
@@ -323,9 +327,10 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
|
||||
## **Releasing**
|
||||
|
||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
||||
2. Install dependencies by running `npm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
3. Build the release files with `npm run generate`.
|
||||
4. Find the built project in `./dist`.
|
||||
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. Build the release files with `pnpm run generate`.
|
||||
5. Find the built project in `packages/hoppscotch-app/dist`.
|
||||
|
||||
## **Contributing**
|
||||
|
||||
|
||||
@@ -10,10 +10,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. **Create a new branch for your translation.**
|
||||
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/locales) directory.**
|
||||
4. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/locales/en.json) to the target language file.**
|
||||
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-app/locales) directory.**
|
||||
4. **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.**
|
||||
5. **Translate the strings in the target language file.**
|
||||
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/languages.json).**
|
||||
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/languages.json).**
|
||||
7. **Save & commit changes.**
|
||||
8. **Send a pull request.**
|
||||
|
||||
|
||||
@@ -1 +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" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>
|
||||
|
Before Width: | Height: | Size: 396 B |
@@ -1,166 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<header
|
||||
class="flex space-x-2 flex-1 py-2 px-2 items-center justify-between"
|
||||
>
|
||||
<div class="space-x-2 inline-flex items-center">
|
||||
<ButtonSecondary
|
||||
class="tracking-wide !font-bold !text-secondaryDark"
|
||||
label="HOPPSCOTCH"
|
||||
to="/"
|
||||
/>
|
||||
<AppGitHubStarButton class="mt-1.5 transition hidden sm:flex" />
|
||||
</div>
|
||||
<div class="space-x-2 inline-flex items-center">
|
||||
<ButtonSecondary
|
||||
id="installPWA"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('header.install_pwa')"
|
||||
svg="download"
|
||||
class="rounded"
|
||||
@click.native="showInstallPrompt()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${$t('app.search')} <kbd>/</kbd>`"
|
||||
svg="search"
|
||||
class="rounded"
|
||||
@click.native="showSearch = true"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${$t('support.title')} <kbd>?</kbd>`"
|
||||
svg="life-buoy"
|
||||
class="rounded"
|
||||
@click.native="showSupport = true"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="currentUser === null"
|
||||
svg="upload-cloud"
|
||||
:label="$t('header.save_workspace')"
|
||||
filled
|
||||
class="hidden !font-semibold md:flex"
|
||||
@click.native="showLogin = true"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
v-if="currentUser === null"
|
||||
:label="$t('header.login')"
|
||||
@click.native="showLogin = true"
|
||||
/>
|
||||
<span v-else class="px-2">
|
||||
<tippy ref="user" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ProfilePicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="currentUser.displayName"
|
||||
:title="currentUser.displayName"
|
||||
indicator
|
||||
:indicator-styles="isOnLine ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('header.account')"
|
||||
class="rounded"
|
||||
svg="user"
|
||||
/>
|
||||
</template>
|
||||
<SmartItem
|
||||
to="/settings"
|
||||
svg="settings"
|
||||
:label="$t('navigation.settings')"
|
||||
@click.native="$refs.user.tippy().hide()"
|
||||
/>
|
||||
<FirebaseLogout @confirm-logout="$refs.user.tippy().hide()" />
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<AppAnnouncement v-if="!isOnLine" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
<AppSupport :show="showSupport" @hide-modal="showSupport = false" />
|
||||
<AppPowerSearch :show="showSearch" @hide-modal="showSearch = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||
import intializePwa from "~/helpers/pwa"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const showSupport = ref(false)
|
||||
const showSearch = ref(false)
|
||||
|
||||
defineActionHandler("modals.support.toggle", () => {
|
||||
showSupport.value = !showSupport.value
|
||||
})
|
||||
defineActionHandler("modals.search.toggle", () => {
|
||||
showSearch.value = !showSearch.value
|
||||
})
|
||||
|
||||
return {
|
||||
currentUser: useReadonlyStream(currentUser$, null),
|
||||
showSupport,
|
||||
showSearch,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Once the PWA code is initialized, this holds a method
|
||||
// that can be called to show the user the installation
|
||||
// prompt.
|
||||
showInstallPrompt: null,
|
||||
showLogin: false,
|
||||
isOnLine: navigator.onLine,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
window.addEventListener("online", () => {
|
||||
this.isOnLine = true
|
||||
})
|
||||
window.addEventListener("offline", () => {
|
||||
this.isOnLine = false
|
||||
})
|
||||
|
||||
// Initializes the PWA code - checks if the app is installed,
|
||||
// etc.
|
||||
this.showInstallPrompt = await intializePwa()
|
||||
|
||||
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
|
||||
if (!cookiesAllowed) {
|
||||
this.$toast.show(this.$t("app.we_use_cookies").toString(), {
|
||||
icon: "cookie",
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: this.$t("action.learn_more").toString(),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
window
|
||||
.open("https://docs.hoppscotch.io/privacy", "_blank")
|
||||
.focus()
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("action.dismiss").toString(),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,243 +0,0 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('collection.save_as')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<div class="flex relative">
|
||||
<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="px-4 pt-4 pb-4">
|
||||
{{ $t("collection.select_location") }}
|
||||
</label>
|
||||
<CollectionsGraphql
|
||||
v-if="mode === 'graphql'"
|
||||
:doc="false"
|
||||
:show-coll-actions="false"
|
||||
:picked="picked"
|
||||
:saving-mode="true"
|
||||
@select="onSelect"
|
||||
/>
|
||||
<Collections
|
||||
v-else
|
||||
:picked="picked"
|
||||
:save-request="true"
|
||||
@select="onSelect"
|
||||
@update-collection="collectionsType.type = $event"
|
||||
@update-coll-type="onUpdateCollType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<ButtonPrimary
|
||||
:label="$t('action.save')"
|
||||
@click.native="saveRequestAs"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
import {
|
||||
saveRESTRequestAs,
|
||||
editRESTRequest,
|
||||
editGraphqlRequest,
|
||||
saveGraphqlRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||
import {
|
||||
getRESTRequest,
|
||||
useRESTRequestName,
|
||||
setRESTSaveContext,
|
||||
} from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// mode can be either "graphql" or "rest"
|
||||
mode: { type: String, default: "rest" },
|
||||
show: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
requestName:
|
||||
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
requestData: {
|
||||
name: this.requestName,
|
||||
collectionIndex: undefined,
|
||||
folderName: undefined,
|
||||
requestIndex: undefined,
|
||||
},
|
||||
collectionsType: {
|
||||
type: "my-collections",
|
||||
selectedTeam: undefined,
|
||||
},
|
||||
picked: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
|
||||
// if user has chosen some folder, than selected other collection, which doesn't have any folders
|
||||
// than `requestUpdateData.folderName` won't be reseted
|
||||
this.$data.requestData.folderName = undefined
|
||||
this.$data.requestData.requestIndex = undefined
|
||||
},
|
||||
"requestData.folderName": function resetRequestIndex() {
|
||||
this.$data.requestData.requestIndex = undefined
|
||||
},
|
||||
editingRequest({ name }) {
|
||||
this.$data.requestData.name = name || this.$data.defaultRequestName
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onUpdateCollType(newCollType) {
|
||||
this.collectionsType = newCollType
|
||||
},
|
||||
onSelect({ picked }) {
|
||||
this.picked = picked
|
||||
},
|
||||
async saveRequestAs() {
|
||||
if (!this.requestName) {
|
||||
this.$toast.error(this.$t("error.empty_req_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.picked == null) {
|
||||
this.$toast.error(this.$t("collection.select"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const requestUpdated =
|
||||
this.mode === "rest" ? getRESTRequest() : getGQLSession()
|
||||
|
||||
// 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 (this.picked.pickedType === "my-request") {
|
||||
editRESTRequest(
|
||||
this.picked.folderPath,
|
||||
this.picked.requestIndex,
|
||||
requestUpdated
|
||||
)
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: this.picked.folderPath,
|
||||
requestIndex: this.picked.requestIndex,
|
||||
})
|
||||
} else if (this.picked.pickedType === "my-folder") {
|
||||
const insertionIndex = saveRESTRequestAs(
|
||||
this.picked.folderPath,
|
||||
requestUpdated
|
||||
)
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: this.picked.folderPath,
|
||||
requestIndex: insertionIndex,
|
||||
})
|
||||
} else if (this.picked.pickedType === "my-collection") {
|
||||
const insertionIndex = saveRESTRequestAs(
|
||||
`${this.picked.collectionIndex}`,
|
||||
requestUpdated
|
||||
)
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: `${this.picked.collectionIndex}`,
|
||||
requestIndex: insertionIndex,
|
||||
})
|
||||
} else if (this.picked.pickedType === "teams-request") {
|
||||
teamUtils.overwriteRequestTeams(
|
||||
this.$apollo,
|
||||
JSON.stringify(requestUpdated),
|
||||
requestUpdated.name,
|
||||
this.picked.requestID
|
||||
)
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: this.picked.requestID,
|
||||
})
|
||||
} else if (this.picked.pickedType === "teams-folder") {
|
||||
const req = await teamUtils.saveRequestAsTeams(
|
||||
this.$apollo,
|
||||
JSON.stringify(requestUpdated),
|
||||
requestUpdated.name,
|
||||
this.collectionsType.selectedTeam.id,
|
||||
this.picked.folderID
|
||||
)
|
||||
|
||||
if (req && req.id) {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: req.id,
|
||||
teamID: this.collectionsType.selectedTeam.id,
|
||||
collectionID: this.picked.folderID,
|
||||
})
|
||||
}
|
||||
} else if (this.picked.pickedType === "teams-collection") {
|
||||
const req = await teamUtils.saveRequestAsTeams(
|
||||
this.$apollo,
|
||||
JSON.stringify(requestUpdated),
|
||||
requestUpdated.name,
|
||||
this.collectionsType.selectedTeam.id,
|
||||
this.picked.collectionID
|
||||
)
|
||||
|
||||
if (req && req.id) {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: req.id,
|
||||
teamID: this.collectionsType.selectedTeam.id,
|
||||
collectionID: this.picked.collectionID,
|
||||
})
|
||||
}
|
||||
} else if (this.picked.pickedType === "gql-my-request") {
|
||||
editGraphqlRequest(
|
||||
this.picked.folderPath,
|
||||
this.picked.requestIndex,
|
||||
requestUpdated
|
||||
)
|
||||
} else if (this.picked.pickedType === "gql-my-folder") {
|
||||
saveGraphqlRequestAs(this.picked.folderPath, requestUpdated)
|
||||
} else if (this.picked.pickedType === "gql-my-collection") {
|
||||
saveGraphqlRequestAs(`${this.picked.collectionIndex}`, requestUpdated)
|
||||
}
|
||||
this.$toast.success(this.$t("request.added"), {
|
||||
icon: "post_add",
|
||||
})
|
||||
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.picked = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,254 +0,0 @@
|
||||
<template>
|
||||
<div class="opacity-0 show-if-initialized" :class="{ initialized }">
|
||||
<pre ref="editor" :class="styles"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import "ace-builds/src-noconflict/ext-language_tools"
|
||||
import "ace-builds/src-noconflict/mode-graphqlschema"
|
||||
import * as gql from "graphql"
|
||||
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
|
||||
import debounce from "~/helpers/utils/debounce"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
onRunGQLQuery: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
editor: null,
|
||||
cacheValue: "",
|
||||
validationSchema: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
appFontSize() {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--body-font-size"
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.cacheValue) {
|
||||
this.editor.session.setValue(value, 1)
|
||||
this.cacheValue = value
|
||||
}
|
||||
},
|
||||
theme() {
|
||||
this.initialized = false
|
||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
},
|
||||
options(value) {
|
||||
this.editor.setOptions(value)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
defineGQLLanguageMode(ace)
|
||||
|
||||
const langTools = ace.require("ace/ext/language_tools")
|
||||
|
||||
const editor = ace.edit(this.$refs.editor, {
|
||||
mode: `ace/mode/gql-query`,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
...this.options,
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
|
||||
editor.setFontSize(this.appFontSize)
|
||||
|
||||
const completer = {
|
||||
getCompletions: (
|
||||
editor,
|
||||
_session,
|
||||
{ row, column },
|
||||
_prefix,
|
||||
callback
|
||||
) => {
|
||||
if (this.validationSchema) {
|
||||
const completions = getAutocompleteSuggestions(
|
||||
this.validationSchema,
|
||||
editor.getValue(),
|
||||
{
|
||||
line: row,
|
||||
character: column,
|
||||
}
|
||||
)
|
||||
|
||||
callback(
|
||||
null,
|
||||
completions.map(({ label, detail }) => ({
|
||||
name: label,
|
||||
value: label,
|
||||
score: 1.0,
|
||||
meta: detail,
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
callback(null, [])
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
langTools.setCompleters([completer])
|
||||
|
||||
if (this.value) editor.setValue(this.value, 1)
|
||||
|
||||
this.editor = editor
|
||||
this.cacheValue = this.value
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: "runGQLQuery",
|
||||
exec: () => this.onRunGQLQuery(this.editor.getValue()),
|
||||
bindKey: {
|
||||
mac: "cmd-enter",
|
||||
win: "ctrl-enter",
|
||||
},
|
||||
})
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: "prettifyGQLQuery",
|
||||
exec: () => this.prettifyQuery(),
|
||||
bindKey: {
|
||||
mac: "cmd-p",
|
||||
win: "ctrl-p",
|
||||
},
|
||||
})
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.parseContents(content)
|
||||
this.cacheValue = content
|
||||
})
|
||||
|
||||
this.parseContents(this.value)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettifyQuery() {
|
||||
try {
|
||||
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
|
||||
} catch (e) {
|
||||
this.$toast.error(this.$t("error.gql_prettify_invalid_query"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
const strip = (str) =>
|
||||
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
|
||||
return strip(
|
||||
window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--editor-theme")
|
||||
)
|
||||
},
|
||||
|
||||
setValidationSchema(schema) {
|
||||
this.validationSchema = schema
|
||||
this.parseContents(this.cacheValue)
|
||||
},
|
||||
|
||||
parseContents: debounce(function (content) {
|
||||
if (content !== "") {
|
||||
try {
|
||||
const doc = gql.parse(content)
|
||||
|
||||
if (this.validationSchema) {
|
||||
this.editor.session.setAnnotations(
|
||||
gql
|
||||
.validate(this.validationSchema, doc)
|
||||
.map(({ locations, message }) => ({
|
||||
row: locations[0].line - 1,
|
||||
column: locations[0].column - 1,
|
||||
text: message,
|
||||
type: "error",
|
||||
}))
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
this.editor.session.setAnnotations([
|
||||
{
|
||||
row: e.locations[0].line - 1,
|
||||
column: e.locations[0].column - 1,
|
||||
text: e.message,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
} else {
|
||||
this.editor.session.setAnnotations([])
|
||||
}
|
||||
}, 2000),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.show-if-initialized {
|
||||
&.initialized {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply transition-none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,188 +0,0 @@
|
||||
<template>
|
||||
<AppSection ref="response" label="response">
|
||||
<div
|
||||
v-if="responseString"
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
pl-4
|
||||
top-0
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.title") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadResponseIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copyResponseButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyResponseIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="responseString"
|
||||
:value="responseString"
|
||||
:lang="'json'"
|
||||
:lint="false"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
flex flex-col flex-1
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<div class="flex flex-col space-y-4 items-end">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.request.send_request") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.show_all") }}
|
||||
</span>
|
||||
<!-- <span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.command_menu") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.help_menu") }}
|
||||
</span> -->
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">G</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">K</span>
|
||||
</div>
|
||||
<!-- <div class="flex">
|
||||
<span class="shortcut-key">/</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">?</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
:label="$t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
svg="external-link"
|
||||
blank
|
||||
outline
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
|
||||
const downloadResponseIcon = ref("download")
|
||||
const copyResponseIcon = ref("copy")
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = "check"
|
||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadResponseIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
responseString,
|
||||
|
||||
downloadResponseIcon,
|
||||
copyResponseIcon,
|
||||
|
||||
downloadResponse,
|
||||
copyResponse,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-key {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply py-1;
|
||||
@apply px-2;
|
||||
@apply inline-flex;
|
||||
}
|
||||
</style>
|
||||
@@ -1,470 +0,0 @@
|
||||
<template>
|
||||
<aside>
|
||||
<SmartTabs styles="sticky bg-primary z-10 top-0">
|
||||
<SmartTab :id="'docs'" :label="`Docs`" :selected="true">
|
||||
<AppSection label="docs">
|
||||
<div class="bg-primary flex top-sidebarPrimaryStickyFold z-10 sticky">
|
||||
<input
|
||||
v-model="graphqlFieldsFilterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="$t('action.search')"
|
||||
class="bg-transparent flex w-full p-4 py-2"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartTabs
|
||||
ref="gqlTabs"
|
||||
styles="border-t border-dividerLight bg-primary sticky z-10 top-sidebarSecondaryStickyFold"
|
||||
>
|
||||
<div class="gqlTabs">
|
||||
<SmartTab
|
||||
v-if="queryFields.length > 0"
|
||||
:id="'queries'"
|
||||
:label="$t('tab.queries')"
|
||||
:selected="true"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredQueryFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="mutationFields.length > 0"
|
||||
:id="'mutations'"
|
||||
:label="$t('graphql.mutations')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredMutationFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="subscriptionFields.length > 0"
|
||||
:id="'subscriptions'"
|
||||
:label="$t('graphql.subscriptions')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredSubscriptionFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="graphqlTypes.length > 0"
|
||||
:id="'types'"
|
||||
ref="typesTab"
|
||||
:label="$t('tab.types')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlType
|
||||
v-for="(type, index) in filteredGraphqlTypes"
|
||||
:key="`type-${index}`"
|
||||
:gql-type="type"
|
||||
:gql-types="graphqlTypes"
|
||||
:is-highlighted="isGqlTypeHighlighted(type)"
|
||||
:highlighted-fields="getGqlTypeHighlightedFields(type)"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
/>
|
||||
</SmartTab>
|
||||
</div>
|
||||
</SmartTabs>
|
||||
<div
|
||||
v-if="
|
||||
queryFields.length === 0 &&
|
||||
mutationFields.length === 0 &&
|
||||
subscriptionFields.length === 0 &&
|
||||
graphqlTypes.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'history'" :label="$t('tab.history')">
|
||||
<History
|
||||
ref="graphqlHistoryComponent"
|
||||
:page="'graphql'"
|
||||
@useHistory="handleUseHistory"
|
||||
/>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'collections'" :label="$t('tab.collections')">
|
||||
<CollectionsGraphql />
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'schema'" :label="`Schema`">
|
||||
<AppSection ref="schema" label="schema">
|
||||
<div
|
||||
v-if="schemaString"
|
||||
class="
|
||||
bg-primary
|
||||
flex flex-1
|
||||
top-sidebarPrimaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("graphql.schema") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="downloadSchema"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadSchemaIcon"
|
||||
@click.native="downloadSchema"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copySchemaCode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copySchemaIcon"
|
||||
@click.native="copySchema"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="schemaString"
|
||||
v-model="schemaString"
|
||||
:lang="'graphqlschema'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GraphQLField, GraphQLType } from "graphql"
|
||||
import { map } from "rxjs/operators"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import {
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLURL,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
function isTextFoundInGraphqlFieldObject(
|
||||
text: string,
|
||||
field: GraphQLField<any, any>
|
||||
) {
|
||||
const normalizedText = text.toLowerCase()
|
||||
|
||||
const isFilterTextFoundInDescription = field.description
|
||||
? field.description.toLowerCase().includes(normalizedText)
|
||||
: false
|
||||
const isFilterTextFoundInName = field.name
|
||||
.toLowerCase()
|
||||
.includes(normalizedText)
|
||||
|
||||
return isFilterTextFoundInDescription || isFilterTextFoundInName
|
||||
}
|
||||
|
||||
function getFilteredGraphqlFields(
|
||||
filterText: string,
|
||||
fields: GraphQLField<any, any>[]
|
||||
) {
|
||||
if (!filterText) return fields
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(filterText, field)
|
||||
)
|
||||
}
|
||||
|
||||
function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
|
||||
if (!filterText) return types
|
||||
|
||||
return types.filter((type) => {
|
||||
const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
|
||||
filterText,
|
||||
type as any
|
||||
)
|
||||
|
||||
if (isFilterTextMatching) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isFilterTextMatchingAtLeastOneField = Object.values(
|
||||
(type as any)._fields || {}
|
||||
).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
|
||||
|
||||
return isFilterTextMatchingAtLeastOneField
|
||||
})
|
||||
}
|
||||
|
||||
function resolveRootType(type: GraphQLType) {
|
||||
let t: any = type
|
||||
while (t.ofType) t = t.ofType
|
||||
return t
|
||||
}
|
||||
|
||||
type GQLHistoryEntry = {
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
query: string
|
||||
response: string
|
||||
variables: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const queryFields = useReadonlyStream(
|
||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const mutationFields = useReadonlyStream(
|
||||
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const subscriptionFields = useReadonlyStream(
|
||||
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const graphqlTypes = useReadonlyStream(
|
||||
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const downloadSchemaIcon = ref("download")
|
||||
const copySchemaIcon = ref("copy")
|
||||
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
|
||||
const gqlTabs = ref<any | null>(null)
|
||||
const typesTab = ref<any | null>(null)
|
||||
|
||||
const filteredQueryFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
queryFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredMutationFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
mutationFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredSubscriptionFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
subscriptionFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredGraphqlTypes = computed(() => {
|
||||
return getFilteredGraphqlTypes(
|
||||
graphqlFieldsFilterText.value,
|
||||
graphqlTypes.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return false
|
||||
|
||||
return isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
gqlType as any
|
||||
)
|
||||
}
|
||||
|
||||
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return []
|
||||
|
||||
const fields = Object.values((gqlType as any)._fields || {})
|
||||
if (!fields || fields.length === 0) return []
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
field as any
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleJumpToType = async (type: GraphQLType) => {
|
||||
gqlTabs.value.selectTab(typesTab.value)
|
||||
await nextTick()
|
||||
|
||||
const rootTypeName = resolveRootType(type).name
|
||||
|
||||
const target = document.getElementById(`type_${rootTypeName}`)
|
||||
if (target) {
|
||||
gqlTabs.value.$el
|
||||
.querySelector(".gqlTabs")
|
||||
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
const schemaString = useReadonlyStream(
|
||||
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||
""
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadSchemaIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
if (!schemaString.value) return
|
||||
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = "check"
|
||||
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
const url = entry.url
|
||||
const headers = entry.headers
|
||||
const gqlQueryString = entry.query
|
||||
const variableString = entry.variables
|
||||
const responseText = entry.response
|
||||
|
||||
setGQLURL(url)
|
||||
setGQLHeaders(headers)
|
||||
setGQLQuery(gqlQueryString)
|
||||
setGQLVariables(variableString)
|
||||
setGQLResponse(responseText)
|
||||
props.conn.reset()
|
||||
}
|
||||
|
||||
return {
|
||||
queryFields,
|
||||
mutationFields,
|
||||
subscriptionFields,
|
||||
graphqlTypes,
|
||||
schemaString,
|
||||
|
||||
graphqlFieldsFilterText,
|
||||
|
||||
filteredQueryFields,
|
||||
filteredMutationFields,
|
||||
filteredSubscriptionFields,
|
||||
filteredGraphqlTypes,
|
||||
|
||||
isGqlTypeHighlighted,
|
||||
getGqlTypeHighlightedFields,
|
||||
|
||||
gqlTabs,
|
||||
typesTab,
|
||||
handleJumpToType,
|
||||
|
||||
downloadSchema,
|
||||
downloadSchemaIcon,
|
||||
copySchemaIcon,
|
||||
copySchema,
|
||||
handleUseHistory,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('import.curl')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<textarea-autosize
|
||||
id="import-curl"
|
||||
v-model="curl"
|
||||
class="font-mono textarea floating-input"
|
||||
autofocus
|
||||
rows="8"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label for="import-curl">
|
||||
{{ $t("request.enter_curl") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<ButtonPrimary
|
||||
:label="$t('import.title')"
|
||||
@click.native="handleImport"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import parseCurlCommand from "~/helpers/curlparser"
|
||||
import {
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
makeRESTRequest,
|
||||
} from "~/helpers/types/HoppRESTRequest"
|
||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
data() {
|
||||
return {
|
||||
curl: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
handleImport() {
|
||||
const text = this.curl
|
||||
try {
|
||||
const parsedCurl = parseCurlCommand(text)
|
||||
const { origin, pathname } = new URL(
|
||||
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
|
||||
)
|
||||
const endpoint = origin + pathname
|
||||
const headers: HoppRESTHeader[] = []
|
||||
const params: HoppRESTParam[] = []
|
||||
if (parsedCurl.query) {
|
||||
for (const key of Object.keys(parsedCurl.query)) {
|
||||
const val = parsedCurl.query[key]!
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((value) => {
|
||||
params.push({
|
||||
key,
|
||||
value,
|
||||
active: true,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
params.push({
|
||||
key,
|
||||
value: val!,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedCurl.headers) {
|
||||
for (const key of Object.keys(parsedCurl.headers)) {
|
||||
headers.push({
|
||||
key,
|
||||
value: parsedCurl.headers[key],
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
const method = parsedCurl.method.toUpperCase()
|
||||
// let rawInput = false
|
||||
// let rawParams: any | null = null
|
||||
|
||||
// if (parsedCurl.data) {
|
||||
// rawInput = true
|
||||
// rawParams = parsedCurl.data
|
||||
// }
|
||||
|
||||
this.showCurlImportModal = false
|
||||
|
||||
setRESTRequest(
|
||||
makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
endpoint,
|
||||
method,
|
||||
params,
|
||||
headers,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: "",
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$toast.error(this.$t("error.curl_invalid_format").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperTertiaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("request.raw_body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent('rawParams', $event)"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="contentType && contentType.endsWith('json')"
|
||||
ref="prettifyRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.prettify')"
|
||||
:svg="prettifyIcon"
|
||||
@click.native="prettifyRequestBody"
|
||||
/>
|
||||
<label for="payload">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('import.json')"
|
||||
svg="file-plus"
|
||||
@click.native="$refs.payload.click()"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
ref="payload"
|
||||
class="input"
|
||||
name="payload"
|
||||
type="file"
|
||||
@change="uploadPayload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
v-model="rawParamsBody"
|
||||
:lang="rawInputEditorLang"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { getEditorLangForMimeType } from "~/helpers/editorutils"
|
||||
import { pluckRef } from "~/helpers/utils/composables"
|
||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
contentType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
|
||||
prettifyIcon: "align-left",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rawInputEditorLang() {
|
||||
return getEditorLangForMimeType(this.contentType)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearContent() {
|
||||
this.rawParamsBody = ""
|
||||
},
|
||||
uploadPayload() {
|
||||
const file = this.$refs.payload.files[0]
|
||||
if (file !== undefined && file !== null) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
this.rawParamsBody = target.result
|
||||
}
|
||||
reader.readAsText(file)
|
||||
this.$toast.success(this.$t("state.file_imported"), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
} else {
|
||||
this.$toast.error(this.$t("action.choose_file"), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
}
|
||||
this.$refs.payload.value = ""
|
||||
},
|
||||
prettifyRequestBody() {
|
||||
try {
|
||||
const jsonObj = JSON.parse(this.rawParamsBody)
|
||||
this.rawParamsBody = JSON.stringify(jsonObj, null, 2)
|
||||
this.prettifyIcon = "check"
|
||||
setTimeout(() => (this.prettifyIcon = "align-left"), 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$toast.error(`${this.$t("error.json_prettify_invalid_body")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-lowerSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
previewEnabled ? $t('hide.preview') : $t('response.preview_html')
|
||||
"
|
||||
:svg="!previewEnabled ? 'eye' : 'eye-off'"
|
||||
@click.native.prevent="togglePreview"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="copyResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'html'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<iframe
|
||||
ref="previewFrame"
|
||||
:class="{ hidden: !previewEnabled }"
|
||||
class="covers-response"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
previewEnabled: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: "text/html" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
togglePreview() {
|
||||
this.previewEnabled = !this.previewEnabled
|
||||
if (this.previewEnabled) {
|
||||
if (
|
||||
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
|
||||
this.url
|
||||
)
|
||||
return
|
||||
// Use DOMParser to parse document HTML.
|
||||
const previewDocument = new DOMParser().parseFromString(
|
||||
this.responseBodyText,
|
||||
"text/html"
|
||||
)
|
||||
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
||||
previewDocument.head.innerHTML =
|
||||
`<base href="${this.url}">` + previewDocument.head.innerHTML
|
||||
// Finally, set the iframe source to the resulting HTML.
|
||||
this.$refs.previewFrame.srcdoc =
|
||||
previewDocument.documentElement.outerHTML
|
||||
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.covers-response {
|
||||
@apply absolute;
|
||||
@apply inset-0;
|
||||
@apply bg-white;
|
||||
@apply h-full;
|
||||
@apply w-full;
|
||||
@apply border;
|
||||
@apply border-dividerLight;
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-lowerSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="copyResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="jsonBodyText"
|
||||
:lang="'json'"
|
||||
:provide-outline="true"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jsonBodyText() {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
|
||||
} catch (e) {
|
||||
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
||||
return this.responseBodyText
|
||||
}
|
||||
},
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-lowerSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="copyResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'plain_text'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-lowerSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="copyResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'xml'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copyIcon: "copy",
|
||||
downloadIcon: "download",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,282 +0,0 @@
|
||||
<template>
|
||||
<div class="show-if-initialized" :class="{ initialized }">
|
||||
<pre ref="editor" :class="styles"></pre>
|
||||
<div
|
||||
v-if="provideOutline"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border-t border-divider
|
||||
flex flex-nowrap flex-1
|
||||
py-1
|
||||
px-4
|
||||
bottom-0
|
||||
z-10
|
||||
sticky
|
||||
overflow-auto
|
||||
hide-scrollbar
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(p, index) in currentPath"
|
||||
:key="`p-${index}`"
|
||||
class="
|
||||
cursor-pointer
|
||||
flex-grow-0 flex-shrink-0
|
||||
text-secondaryLight
|
||||
inline-flex
|
||||
items-center
|
||||
hover:text-secondary
|
||||
"
|
||||
>
|
||||
<span @click="onBlockClick(index)">
|
||||
{{ p }}
|
||||
</span>
|
||||
<i v-if="index + 1 !== currentPath.length" class="mx-2 material-icons">
|
||||
chevron_right
|
||||
</i>
|
||||
<tippy
|
||||
v-if="siblingDropDownIndex == index"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<SmartItem
|
||||
v-for="(sibling, siblingIndex) in currentSibling"
|
||||
:key="`p-${index}-sibling-${siblingIndex}`"
|
||||
:label="sibling.key ? sibling.key.value : i"
|
||||
@click.native="goToSibling(sibling)"
|
||||
/>
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import jsonParse from "~/helpers/jsonParse"
|
||||
import debounce from "~/helpers/utils/debounce"
|
||||
import outline from "~/helpers/outline"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
provideOutline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: "json",
|
||||
},
|
||||
lint: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
editor: null,
|
||||
cacheValue: "",
|
||||
outline: outline(),
|
||||
currentPath: [],
|
||||
currentSibling: [],
|
||||
siblingDropDownIndex: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
appFontSize() {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--body-font-size"
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.cacheValue) {
|
||||
this.editor.session.setValue(value, 1)
|
||||
this.cacheValue = value
|
||||
if (this.lint) this.provideLinting(value)
|
||||
}
|
||||
},
|
||||
theme() {
|
||||
this.initialized = false
|
||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
},
|
||||
lang(value) {
|
||||
this.editor.getSession().setMode(`ace/mode/${value}`)
|
||||
},
|
||||
options(value) {
|
||||
this.editor.setOptions(value)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const editor = ace.edit(this.$refs.editor, {
|
||||
mode: `ace/mode/${this.lang}`,
|
||||
...this.options,
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
|
||||
editor.setFontSize(this.appFontSize)
|
||||
|
||||
if (this.value) editor.setValue(this.value, 1)
|
||||
|
||||
this.editor = editor
|
||||
this.cacheValue = this.value
|
||||
|
||||
if (this.lang === "json" && this.provideOutline)
|
||||
this.initOutline(this.value)
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.cacheValue = content
|
||||
|
||||
if (this.provideOutline) debounce(this.initOutline(content), 500)
|
||||
|
||||
if (this.lint) this.provideLinting(content)
|
||||
})
|
||||
|
||||
if (this.lang === "json" && this.provideOutline) {
|
||||
editor.session.selection.on("changeCursor", () => {
|
||||
const index = editor.session.doc.positionToIndex(
|
||||
editor.selection.getCursor(),
|
||||
0
|
||||
)
|
||||
const path = this.outline.genPath(index)
|
||||
if (path.success) {
|
||||
this.currentPath = path.res
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Disable linting, if lint prop is false
|
||||
if (this.lint) this.provideLinting(this.value)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
|
||||
methods: {
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
const strip = (str) =>
|
||||
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
|
||||
return strip(
|
||||
window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--editor-theme")
|
||||
)
|
||||
},
|
||||
|
||||
provideLinting: debounce(function (code) {
|
||||
if (this.lang === "json") {
|
||||
try {
|
||||
jsonParse(code)
|
||||
this.editor.session.setAnnotations([])
|
||||
} catch (e) {
|
||||
const pos = this.editor.session
|
||||
.getDocument()
|
||||
.indexToPosition(e.start, 0)
|
||||
this.editor.session.setAnnotations([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: e.message,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}, 2000),
|
||||
|
||||
onBlockClick(index) {
|
||||
if (this.siblingDropDownIndex === index) {
|
||||
this.clearSiblingList()
|
||||
} else {
|
||||
this.currentSibling = this.outline.getSiblings(index)
|
||||
if (this.currentSibling.length) this.siblingDropDownIndex = index
|
||||
}
|
||||
},
|
||||
clearSiblingList() {
|
||||
this.currentSibling = []
|
||||
this.siblingDropDownIndex = null
|
||||
},
|
||||
goToSibling(obj) {
|
||||
this.clearSiblingList()
|
||||
if (obj.start) {
|
||||
const pos = this.editor.session.doc.indexToPosition(obj.start, 0)
|
||||
if (pos) {
|
||||
this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
|
||||
this.editor.session.selection.clearSelection()
|
||||
this.editor.scrollToLine(pos.row, false, true, null)
|
||||
}
|
||||
}
|
||||
},
|
||||
initOutline: debounce(function (content) {
|
||||
if (this.lang === "json") {
|
||||
try {
|
||||
this.outline.init(content)
|
||||
|
||||
if (content[0] === "[") this.currentPath.push("[]")
|
||||
else this.currentPath.push("{}")
|
||||
} catch (e) {
|
||||
console.log("Outline error: ", e)
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.show-if-initialized {
|
||||
&.initialized {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply transition-none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,292 +0,0 @@
|
||||
<template>
|
||||
<div class="show-if-initialized" :class="{ initialized }">
|
||||
<pre ref="editor" :class="styles"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import "ace-builds/src-noconflict/ext-language_tools"
|
||||
import "ace-builds/src-noconflict/mode-graphqlschema"
|
||||
import * as esprima from "esprima"
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import debounce from "~/helpers/utils/debounce"
|
||||
import {
|
||||
getPreRequestScriptCompletions,
|
||||
getTestScriptCompletions,
|
||||
performPreRequestLinting,
|
||||
performTestLinting,
|
||||
} from "~/helpers/tern"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
completeMode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "none",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
editor: null,
|
||||
cacheValue: "",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
appFontSize() {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--body-font-size"
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.cacheValue) {
|
||||
this.editor.session.setValue(value, 1)
|
||||
this.cacheValue = value
|
||||
if (this.lint) this.provideLinting(value)
|
||||
}
|
||||
},
|
||||
theme() {
|
||||
this.initialized = false
|
||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick()
|
||||
.then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
// nextTick shouldn't really ever throw but still
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
},
|
||||
options(value) {
|
||||
this.editor.setOptions(value)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// const langTools = ace.require("ace/ext/language_tools")
|
||||
|
||||
const editor = ace.edit(this.$refs.editor, {
|
||||
mode: `ace/mode/javascript`,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
...this.options,
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick()
|
||||
.then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
// nextTIck shouldn't really ever throw but still
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
|
||||
editor.setFontSize(this.appFontSize)
|
||||
|
||||
const completer = {
|
||||
getCompletions: (
|
||||
editor,
|
||||
_session,
|
||||
{ row, column },
|
||||
_prefix,
|
||||
callback
|
||||
) => {
|
||||
if (this.completeMode === "pre") {
|
||||
getPreRequestScriptCompletions(editor.getValue(), row, column)
|
||||
.then((res) => {
|
||||
callback(
|
||||
null,
|
||||
res.completions.map((r, index, arr) => ({
|
||||
name: r.name,
|
||||
value: r.name,
|
||||
score: (arr.length - index) / arr.length,
|
||||
meta: r.type,
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(() => callback(null, []))
|
||||
} else if (this.completeMode === "test") {
|
||||
getTestScriptCompletions(editor.getValue(), row, column)
|
||||
.then((res) => {
|
||||
callback(
|
||||
null,
|
||||
res.completions.map((r, index, arr) => ({
|
||||
name: r.name,
|
||||
value: r.name,
|
||||
score: (arr.length - index) / arr.length,
|
||||
meta: r.type,
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(() => callback(null, []))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
editor.completers = [completer]
|
||||
|
||||
if (this.value) editor.setValue(this.value, 1)
|
||||
|
||||
this.editor = editor
|
||||
this.cacheValue = this.value
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.cacheValue = content
|
||||
this.provideLinting(content)
|
||||
})
|
||||
|
||||
this.provideLinting(this.value)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
|
||||
methods: {
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
const strip = (str) =>
|
||||
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
|
||||
return strip(
|
||||
window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--editor-theme")
|
||||
)
|
||||
},
|
||||
|
||||
provideLinting: debounce(function (code) {
|
||||
let results = []
|
||||
|
||||
const lintFunc =
|
||||
this.completeMode === "pre"
|
||||
? performPreRequestLinting
|
||||
: performTestLinting
|
||||
|
||||
lintFunc(code)
|
||||
.then((semanticLints) => {
|
||||
results = results.concat(
|
||||
semanticLints.map((lint) => ({
|
||||
row: lint.from.line,
|
||||
column: lint.from.ch,
|
||||
text: `[semantic] ${lint.message}`,
|
||||
type: "error",
|
||||
}))
|
||||
)
|
||||
|
||||
try {
|
||||
const res = esprima.parseScript(code, { tolerant: true })
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
results = results.concat(
|
||||
res.errors.map((err) => {
|
||||
const pos = this.editor.session
|
||||
.getDocument()
|
||||
.indexToPosition(err.index, 0)
|
||||
|
||||
return {
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${err.description}`,
|
||||
type: "error",
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
const pos = this.editor.session
|
||||
.getDocument()
|
||||
.indexToPosition(e.index, 0)
|
||||
results = results.concat([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${e.description}`,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
this.editor.session.setAnnotations(results)
|
||||
})
|
||||
.catch(() => {
|
||||
try {
|
||||
const res = esprima.parseScript(code, { tolerant: true })
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
results = results.concat(
|
||||
res.errors.map((err) => {
|
||||
const pos = this.editor.session
|
||||
.getDocument()
|
||||
.indexToPosition(err.index, 0)
|
||||
|
||||
return {
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${err.description}`,
|
||||
type: "error",
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
const pos = this.editor.session
|
||||
.getDocument()
|
||||
.indexToPosition(e.index, 0)
|
||||
results = results.concat([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${e.description}`,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
this.editor.session.setAnnotations(results)
|
||||
})
|
||||
}, 2000),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.show-if-initialized {
|
||||
&.initialized {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply transition-none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
class="h-4 animate-spin w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('team.new')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<input
|
||||
id="selectLabelTeamAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewTeam"
|
||||
/>
|
||||
<label for="selectLabelTeamAdd">
|
||||
{{ $t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<ButtonPrimary :label="$t('action.save')" @click.native="addNewTeam" />
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewTeam() {
|
||||
// We save the user input in case of an error
|
||||
const name = this.name
|
||||
// We clear it early to give the UI a snappy feel
|
||||
this.name = ""
|
||||
if (!name) {
|
||||
this.$toast.error(this.$t("empty.team_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
if (name !== null && name.replace(/\s/g, "").length < 6) {
|
||||
this.$toast.error(this.$t("team.name_length_insufficient"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.createTeam(this.$apollo, name)
|
||||
.then(() => {
|
||||
this.hideModal()
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
// We restore the initial user input
|
||||
this.name = name
|
||||
})
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,388 +0,0 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('team.edit')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="selectLabelTeamEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveTeam"
|
||||
/>
|
||||
<label for="selectLabelTeamEdit">
|
||||
{{ $t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-1 justify-between items-center">
|
||||
<label for="memberList" class="p-4">
|
||||
{{ $t("team.members") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
svg="plus"
|
||||
:label="$t('add.new')"
|
||||
@click.native="addTeamMember"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y divide-dividerLight border-divider border rounded">
|
||||
<div
|
||||
v-for="(member, index) in members"
|
||||
:key="`member-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'param' + index"
|
||||
:value="member.user.email"
|
||||
readonly
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
:ref="`memberOptions-${index}`"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof member.role === 'string'
|
||||
? member.role
|
||||
: JSON.stringify(member.role)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="updateMemberRole(index, 'OWNER')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="updateMemberRole(index, 'EDITOR')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="updateMemberRole(index, 'VIEWER')"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeExistingTeamMember(member.user.uid)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in newMembers"
|
||||
:key="`new-member-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
v-model="member.key"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'member' + index"
|
||||
autofocus
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
:ref="`newMemberOptions-${index}`"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof member.value === 'string'
|
||||
? member.value
|
||||
: JSON.stringify(member.value)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="updateNewMemberRole(index, 'OWNER')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="updateNewMemberRole(index, 'EDITOR')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="updateNewMemberRole(index, 'VIEWER')"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeTeamMember(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="members.length === 0 && newMembers.length === 0"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<SmartIcon class="opacity-75 pb-2" name="users" />
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.members") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="$t('add.new')"
|
||||
filled
|
||||
@click.native="addTeamMember"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<ButtonPrimary :label="$t('action.save')" @click.native="saveTeam" />
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
import TeamMemberAdapter from "~/helpers/teams/TeamMemberAdapter"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
editingTeam: { type: Object, default: () => {} },
|
||||
editingteamID: { type: String, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rename: null,
|
||||
members: [],
|
||||
newMembers: [],
|
||||
membersAdapter: new TeamMemberAdapter(null),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editingTeamCopy() {
|
||||
return this.editingTeam
|
||||
},
|
||||
name: {
|
||||
get() {
|
||||
return this.editingTeam.name
|
||||
},
|
||||
set(name) {
|
||||
this.rename = name
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editingteamID(teamID) {
|
||||
this.membersAdapter.changeTeamID(teamID)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.membersAdapter.members$.subscribe((list) => {
|
||||
this.members = cloneDeep(list)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateMemberRole(id, role) {
|
||||
this.members[id].role = role
|
||||
this.$refs[`memberOptions-${id}`][0].tippy().hide()
|
||||
},
|
||||
updateNewMemberRole(id, role) {
|
||||
this.newMembers[id].value = role
|
||||
this.$refs[`newMemberOptions-${id}`][0].tippy().hide()
|
||||
},
|
||||
addTeamMember() {
|
||||
const member = { key: "", value: "" }
|
||||
this.newMembers.push(member)
|
||||
},
|
||||
removeExistingTeamMember(userID) {
|
||||
teamUtils
|
||||
.removeTeamMember(this.$apollo, userID, this.editingteamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.member_removed"), {
|
||||
icon: "done",
|
||||
})
|
||||
this.hideModal()
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
},
|
||||
removeTeamMember(index) {
|
||||
this.newMembers.splice(index, 1)
|
||||
},
|
||||
validateEmail(emailID) {
|
||||
if (
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(
|
||||
emailID
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
saveTeam() {
|
||||
if (
|
||||
this.$data.rename !== null &&
|
||||
this.$data.rename.replace(/\s/g, "").length < 6
|
||||
) {
|
||||
this.$toast.error(this.$t("team.name_length_insufficient"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
let invalidEmail = false
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
if (!this.validateEmail(element.key)) {
|
||||
this.$toast.error(this.$t("team.invalid_email_format"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
invalidEmail = true
|
||||
}
|
||||
})
|
||||
if (invalidEmail) return
|
||||
let invalidPermission = false
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
if (!element.value) {
|
||||
this.$toast.error(this.$t("invalid_member_permission"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
invalidPermission = true
|
||||
}
|
||||
})
|
||||
if (invalidPermission) return
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.addTeamMemberByEmail(
|
||||
this.$apollo,
|
||||
element.value,
|
||||
element.key,
|
||||
this.editingteamID
|
||||
)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.saved"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(e, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
this.members.forEach((element) => {
|
||||
teamUtils
|
||||
.updateTeamMemberRole(
|
||||
this.$apollo,
|
||||
element.user.uid,
|
||||
element.role,
|
||||
this.editingteamID
|
||||
)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.member_role_updated"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(e, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
if (this.$data.rename !== null) {
|
||||
const newName =
|
||||
this.name === this.$data.rename ? this.name : this.$data.rename
|
||||
if (!/\S/.test(newName))
|
||||
return this.$toast.error(this.$t("empty.team_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
// Call to the graphql mutation
|
||||
if (this.name !== this.rename)
|
||||
teamUtils
|
||||
.renameTeam(this.$apollo, newName, this.editingteamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.saved"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.rename = null
|
||||
this.newMembers = []
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
<div class="border border-dividerLight rounded flex flex-1 items-end">
|
||||
<div class="flex flex-1 items-start">
|
||||
<div class="p-4">
|
||||
<label
|
||||
class="cursor-pointer transition hover:text-secondaryDark"
|
||||
@click="team.myRole === 'OWNER' ? $emit('edit-team') : ''"
|
||||
>
|
||||
{{ team.name || $t("state.nothing_found") }}
|
||||
</label>
|
||||
<div class="flex -space-x-1 mt-2 overflow-hidden">
|
||||
<img
|
||||
v-for="(member, index) in team.members"
|
||||
:key="`member-${index}`"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="member.user.displayName"
|
||||
:src="member.user.photoURL"
|
||||
:alt="member.user.displayName"
|
||||
class="rounded-full h-5 ring-primary ring-2 w-5 inline-block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.more')"
|
||||
svg="more-vertical"
|
||||
/>
|
||||
</template>
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="edit"
|
||||
:label="$t('action.edit')"
|
||||
@click.native="
|
||||
$emit('edit-team')
|
||||
$refs.options.tippy().hide()
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="trash-2"
|
||||
color="red"
|
||||
:label="$t('action.delete')"
|
||||
@click.native="
|
||||
deleteTeam()
|
||||
$refs.options.tippy().hide()
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
|
||||
svg="trash"
|
||||
:label="$t('team.exit')"
|
||||
@click.native="
|
||||
exitTeam()
|
||||
$refs.options.tippy().hide()
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
team: { type: Object, default: () => {} },
|
||||
teamID: { type: String, default: null },
|
||||
},
|
||||
methods: {
|
||||
deleteTeam() {
|
||||
if (!confirm(this.$t("confirm.remove_team"))) return
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.deleteTeam(this.$apollo, this.teamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.deleted"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
},
|
||||
exitTeam() {
|
||||
if (!confirm("Are you sure you want to exit this team?")) return
|
||||
teamUtils
|
||||
.exitTeam(this.$apollo, this.teamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.left"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<AppSection label="teams">
|
||||
<h4 class="text-secondaryDark">
|
||||
{{ $t("team.title") }}
|
||||
</h4>
|
||||
<div class="mt-1 text-secondaryLight">
|
||||
<SmartAnchor
|
||||
:label="$t('team.join_beta')"
|
||||
to="https://hoppscotch.io/beta"
|
||||
blank
|
||||
class="link"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4 mt-4">
|
||||
<ButtonSecondary
|
||||
:label="$t('team.create_new')"
|
||||
outline
|
||||
@click.native="displayModalAdd(true)"
|
||||
/>
|
||||
<p v-if="$apollo.queries.myTeams.loading">
|
||||
{{ $t("state.loading") }}
|
||||
</p>
|
||||
<div v-if="myTeams.length === 0" class="flex items-center">
|
||||
<i class="mr-4 material-icons">help_outline</i>
|
||||
{{ $t("empty.teams") }}
|
||||
</div>
|
||||
<div v-else class="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<TeamsTeam
|
||||
v-for="(team, index) in myTeams"
|
||||
:key="`team-${index}`"
|
||||
:team-i-d="team.id"
|
||||
:team="team"
|
||||
@edit-team="editTeam(team, team.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
|
||||
<TeamsEdit
|
||||
:team="myTeams[0]"
|
||||
:show="showModalEdit"
|
||||
:editing-team="editingTeam"
|
||||
:editingteam-i-d="editingteamID"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import gql from "graphql-tag"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
currentUser: useReadonlyStream(currentUser$, null),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModalAdd: false,
|
||||
showModalEdit: false,
|
||||
editingTeam: {},
|
||||
editingteamID: "",
|
||||
me: {},
|
||||
myTeams: [],
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
me: {
|
||||
query: gql`
|
||||
query GetMe {
|
||||
me {
|
||||
uid
|
||||
eaInvited
|
||||
}
|
||||
}
|
||||
`,
|
||||
pollInterval: 100000,
|
||||
},
|
||||
myTeams: {
|
||||
query: gql`
|
||||
query GetMyTeams {
|
||||
myTeams {
|
||||
id
|
||||
name
|
||||
myRole
|
||||
ownersCount
|
||||
members {
|
||||
user {
|
||||
photoURL
|
||||
displayName
|
||||
email
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
pollInterval: 10000,
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this._keyListener)
|
||||
},
|
||||
methods: {
|
||||
displayModalAdd(shouldDisplay) {
|
||||
this.showModalAdd = shouldDisplay
|
||||
},
|
||||
displayModalEdit(shouldDisplay) {
|
||||
this.showModalEdit = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
editTeam(team, teamID) {
|
||||
this.editingTeam = team
|
||||
this.editingteamID = teamID
|
||||
this.displayModalEdit(true)
|
||||
},
|
||||
resetSelectedData() {
|
||||
this.$data.editingTeam = undefined
|
||||
this.$data.editingteamID = undefined
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -4,8 +4,10 @@
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"hosting": {
|
||||
"predeploy": ["mv .env.example .env && npm ci && npm run generate"],
|
||||
"public": "dist",
|
||||
"predeploy": [
|
||||
"cd packages/hoppscotch-app && mv .env.example .env && cd ../.. && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||
],
|
||||
"public": "packages/hoppscotch-app/dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { Observable } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import getEnvironmentVariablesFromScript from "./preRequest"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import runTestScriptWithVariables, {
|
||||
transformResponseForTesting,
|
||||
} from "./postwomanTesting"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
* Runs a REST network request along with all the
|
||||
* other side processes (like running test scripts)
|
||||
*/
|
||||
export function runRESTRequest$(): Observable<HoppRESTResponse> {
|
||||
const envs = getEnvironmentVariablesFromScript(
|
||||
getRESTRequest().preRequestScript
|
||||
)
|
||||
|
||||
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
|
||||
name: "Env",
|
||||
variables: Object.keys(envs).map((key) => {
|
||||
return {
|
||||
key,
|
||||
value: envs[key],
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||
|
||||
// Run Test Script when request ran successfully
|
||||
const subscription = stream
|
||||
.pipe(filter((res) => res.type === "success"))
|
||||
.subscribe((res) => {
|
||||
const testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
} = runTestScriptWithVariables(effectiveRequest.testScript, {
|
||||
response: transformResponseForTesting(res),
|
||||
}) as any
|
||||
|
||||
setRESTTestResults(translateToNewTestResults(testReport))
|
||||
|
||||
subscription.unsubscribe()
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
function isTestPass(x: any): x is {
|
||||
result: "PASS"
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "PASS"
|
||||
}
|
||||
|
||||
function isTestFail(x: any): x is {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "FAIL"
|
||||
}
|
||||
|
||||
function isStartBlock(
|
||||
x: any
|
||||
): x is { startBlock: string; styles: { icon: ""; class: "" } } {
|
||||
return x.startBlock !== undefined
|
||||
}
|
||||
|
||||
function isEndBlock(
|
||||
x: any
|
||||
): x is { endBlock: true; styles: { icon: ""; class: "" } } {
|
||||
return x.endBlock !== undefined
|
||||
}
|
||||
|
||||
function translateToNewTestResults(testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
}): HoppTestResult {
|
||||
// Build a stack of test data which we eventually build up based on the results
|
||||
const testsStack: HoppTestData[] = [
|
||||
{
|
||||
description: "root",
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
},
|
||||
]
|
||||
|
||||
testReport.testResults.forEach((result) => {
|
||||
// This is a test block start, push an empty test to the stack
|
||||
if (isStartBlock(result)) {
|
||||
testsStack.push({
|
||||
description: result.startBlock,
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
})
|
||||
} else if (isEndBlock(result)) {
|
||||
// End of the block, pop the stack and add it as a child to the current stack top
|
||||
const testData = testsStack.pop()!
|
||||
testsStack[testsStack.length - 1].tests.push(testData)
|
||||
} else if (isTestPass(result)) {
|
||||
// A normal PASS expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: result.message,
|
||||
})
|
||||
} else if (isTestFail(result)) {
|
||||
// A normal FAIL expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: result.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// We should end up with only the root stack entry
|
||||
if (testsStack.length !== 1) throw new Error("Invalid test result structure")
|
||||
|
||||
return {
|
||||
expectResults: testsStack[0].expectResults,
|
||||
tests: testsStack[0].tests,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
const mimeToMode = {
|
||||
"text/plain": "plain_text",
|
||||
"text/html": "html",
|
||||
"application/xml": "xml",
|
||||
"application/hal+json": "json",
|
||||
"application/vnd.api+json": "json",
|
||||
"application/json": "json",
|
||||
}
|
||||
|
||||
export function getEditorLangForMimeType(mimeType) {
|
||||
return mimeToMode[mimeType] || "plain_text"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import jsonLens from "./jsonLens"
|
||||
import rawLens from "./rawLens"
|
||||
import imageLens from "./imageLens"
|
||||
import htmlLens from "./htmlLens"
|
||||
import xmlLens from "./xmlLens"
|
||||
|
||||
export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
|
||||
|
||||
export function getSuitableLenses(response) {
|
||||
const contentType = response.headers.find((h) => h.key === "content-type")
|
||||
|
||||
if (!contentType) return [rawLens]
|
||||
|
||||
const result = []
|
||||
for (const lens of lenses) {
|
||||
if (lens.isSupportedContentType(contentType.value)) result.push(lens)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getLensRenderers() {
|
||||
const response = {}
|
||||
for (const lens of lenses) {
|
||||
response[lens.renderer] = lens.rendererImport
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
const rawLens = {
|
||||
lensName: "response.raw",
|
||||
isSupportedContentType: () => true,
|
||||
renderer: "raw",
|
||||
rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"),
|
||||
}
|
||||
|
||||
export default rawLens
|
||||
@@ -1,124 +0,0 @@
|
||||
import jsonParse from "./jsonParse"
|
||||
|
||||
export default () => {
|
||||
let jsonAST = {}
|
||||
let path = []
|
||||
|
||||
const init = (jsonStr) => {
|
||||
jsonAST = jsonParse(jsonStr)
|
||||
linkParents(jsonAST)
|
||||
}
|
||||
|
||||
const setNewText = (jsonStr) => {
|
||||
init(jsonStr)
|
||||
path = []
|
||||
}
|
||||
|
||||
const linkParents = (node) => {
|
||||
if (node.kind === "Object") {
|
||||
if (node.members) {
|
||||
node.members.forEach((m) => {
|
||||
m.parent = node
|
||||
linkParents(m)
|
||||
})
|
||||
}
|
||||
} else if (node.kind === "Array") {
|
||||
if (node.values) {
|
||||
node.values.forEach((v) => {
|
||||
v.parent = node
|
||||
linkParents(v)
|
||||
})
|
||||
}
|
||||
} else if (node.kind === "Member") {
|
||||
if (node.value) {
|
||||
node.value.parent = node
|
||||
linkParents(node.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const genPath = (index) => {
|
||||
let output = {}
|
||||
path = []
|
||||
let current = jsonAST
|
||||
if (current.kind === "Object") {
|
||||
path.push({ label: "{}", obj: "root" })
|
||||
} else if (current.kind === "Array") {
|
||||
path.push({ label: "[]", obj: "root" })
|
||||
}
|
||||
let over = false
|
||||
|
||||
try {
|
||||
while (!over) {
|
||||
if (current.kind === "Object") {
|
||||
let i = 0
|
||||
let found = false
|
||||
while (i < current.members.length) {
|
||||
const m = current.members[i]
|
||||
if (m.start <= index && m.end >= index) {
|
||||
path.push({ label: m.key.value, obj: m })
|
||||
current = current.members[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (!found) over = true
|
||||
} else if (current.kind === "Array") {
|
||||
if (current.values) {
|
||||
let i = 0
|
||||
let found = false
|
||||
while (i < current.values.length) {
|
||||
const m = current.values[i]
|
||||
if (m.start <= index && m.end >= index) {
|
||||
path.push({ label: `[${i.toString()}]`, obj: m })
|
||||
current = current.values[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (!found) over = true
|
||||
} else over = true
|
||||
} else if (current.kind === "Member") {
|
||||
if (current.value) {
|
||||
if (current.value.start <= index && current.value.end >= index) {
|
||||
current = current.value
|
||||
} else over = true
|
||||
} else over = true
|
||||
} else if (
|
||||
current.kind === "String" ||
|
||||
current.kind === "Number" ||
|
||||
current.kind === "Boolean" ||
|
||||
current.kind === "Null"
|
||||
) {
|
||||
if (current.start <= index && current.end >= index) {
|
||||
path.push({ label: `${current.value}`, obj: current })
|
||||
}
|
||||
over = true
|
||||
}
|
||||
}
|
||||
output = { success: true, res: path.map((p) => p.label) }
|
||||
} catch (e) {
|
||||
output = { success: false, res: e }
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const getSiblings = (index) => {
|
||||
const parent = path[index]?.obj?.parent
|
||||
if (!parent) return []
|
||||
else if (parent.kind === "Object") {
|
||||
return parent.members
|
||||
} else if (parent.kind === "Array") {
|
||||
return parent.values
|
||||
} else return []
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
genPath,
|
||||
getSiblings,
|
||||
setNewText,
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
|
||||
const styles = {
|
||||
PASS: { icon: "check", class: "success-response" },
|
||||
FAIL: { icon: "close", class: "cl-error-response" },
|
||||
ERROR: { icon: "close", class: "cl-error-response" },
|
||||
}
|
||||
|
||||
type TestScriptResponse = {
|
||||
body: any
|
||||
headers: any[]
|
||||
status: number
|
||||
|
||||
__newRes: HoppRESTResponse
|
||||
}
|
||||
|
||||
type TestScriptVariables = {
|
||||
response: TestScriptResponse
|
||||
}
|
||||
|
||||
type TestReportStartBlock = {
|
||||
startBlock: string
|
||||
}
|
||||
|
||||
type TestReportEndBlock = {
|
||||
endBlock: true
|
||||
}
|
||||
|
||||
type TestReportEntry = {
|
||||
result: "PASS" | "FAIL" | "ERROR"
|
||||
message: string
|
||||
styles: {
|
||||
icon: string
|
||||
}
|
||||
}
|
||||
|
||||
type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
|
||||
|
||||
export default function runTestScriptWithVariables(
|
||||
script: string,
|
||||
variables?: TestScriptVariables
|
||||
) {
|
||||
const pw = {
|
||||
_errors: [],
|
||||
_testReports: [] as TestReport[],
|
||||
_report: "",
|
||||
expect(value: any) {
|
||||
try {
|
||||
return expect(value, this._testReports)
|
||||
} catch (e) {
|
||||
pw._testReports.push({
|
||||
result: "ERROR",
|
||||
message: e.toString(),
|
||||
styles: styles.ERROR,
|
||||
})
|
||||
}
|
||||
},
|
||||
test: (descriptor: string, func: () => void) =>
|
||||
test(descriptor, func, pw._testReports),
|
||||
// globals that the script is allowed to have access to.
|
||||
}
|
||||
Object.assign(pw, variables)
|
||||
|
||||
// run pre-request script within this function so that it has access to the pw object.
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function("pw", script)(pw)
|
||||
|
||||
return {
|
||||
report: pw._report,
|
||||
errors: pw._errors,
|
||||
testResults: pw._testReports,
|
||||
}
|
||||
}
|
||||
|
||||
function test(
|
||||
descriptor: string,
|
||||
func: () => void,
|
||||
_testReports: TestReport[]
|
||||
) {
|
||||
_testReports.push({ startBlock: descriptor })
|
||||
try {
|
||||
func()
|
||||
} catch (e) {
|
||||
_testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
|
||||
}
|
||||
_testReports.push({ endBlock: true })
|
||||
|
||||
// TODO: Organize and generate text report of each {descriptor: true} section in testReports.
|
||||
// add checkmark or x depending on if each testReport is pass=true or pass=false
|
||||
}
|
||||
|
||||
function expect(expectValue: any, _testReports: TestReport[]) {
|
||||
return new Expectation(expectValue, null, _testReports)
|
||||
}
|
||||
|
||||
class Expectation {
|
||||
private expectValue: any
|
||||
private not: true | Expectation
|
||||
private _testReports: TestReport[]
|
||||
|
||||
constructor(
|
||||
expectValue: any,
|
||||
_not: boolean | null,
|
||||
_testReports: TestReport[]
|
||||
) {
|
||||
this.expectValue = expectValue
|
||||
this.not = _not || new Expectation(this.expectValue, true, _testReports)
|
||||
this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
|
||||
}
|
||||
|
||||
private _satisfies(expectValue: any, targetValue?: any): boolean {
|
||||
// Used for testing if two values match the expectation, which could be === OR !==, depending on if not
|
||||
// was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
|
||||
// Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
|
||||
if (!targetValue) {
|
||||
targetValue = expectValue
|
||||
expectValue = this.expectValue
|
||||
}
|
||||
if (this.not === true) {
|
||||
// test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
|
||||
return expectValue !== targetValue
|
||||
} else {
|
||||
return expectValue === targetValue
|
||||
}
|
||||
}
|
||||
|
||||
_fmtNot(message: string) {
|
||||
// given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
|
||||
if (this.not === true) {
|
||||
return message.replace("(not)", "not ")
|
||||
} else {
|
||||
return message.replace("(not)", "")
|
||||
}
|
||||
}
|
||||
|
||||
_fail(message: string) {
|
||||
return this._testReports.push({
|
||||
result: "FAIL",
|
||||
message,
|
||||
styles: styles.FAIL,
|
||||
})
|
||||
}
|
||||
|
||||
_pass(message: string) {
|
||||
return this._testReports.push({
|
||||
result: "PASS",
|
||||
message,
|
||||
styles: styles.PASS,
|
||||
})
|
||||
}
|
||||
|
||||
// TEST METHODS DEFINED BELOW
|
||||
// these are the usual methods that would follow expect(...)
|
||||
toBe(value: any) {
|
||||
return this._satisfies(value)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
|
||||
)
|
||||
}
|
||||
|
||||
toHaveProperty(value: string) {
|
||||
return this._satisfies(
|
||||
Object.prototype.hasOwnProperty.call(this.expectValue, value),
|
||||
true
|
||||
)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected object ${this.expectValue} to (not)have property ${value}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel2xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 200-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 200 && code < 300, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 200-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel3xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 300-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 300 && code < 400, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 300-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel4xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 400-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 400 && code < 500, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 400-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel5xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 500-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 500 && code < 600, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 500-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toHaveLength(expectedLength: number) {
|
||||
const actualLength = this.expectValue.length
|
||||
return this._satisfies(actualLength, expectedLength)
|
||||
? this._pass(
|
||||
this._fmtNot(
|
||||
`Length expectation of (not)being ${expectedLength} is kept`
|
||||
)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected length to be ${expectedLength} but actual length was ${actualLength}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeType(expectedType: string) {
|
||||
const actualType = typeof this.expectValue
|
||||
if (
|
||||
![
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
return this._fail(
|
||||
this._fmtNot(
|
||||
`Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
|
||||
)
|
||||
)
|
||||
}
|
||||
return this._satisfies(actualType, expectedType)
|
||||
? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected type to be "${expectedType}" but actual type was "${actualType}"`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function transformResponseForTesting(
|
||||
response: HoppRESTResponse
|
||||
): TestScriptResponse {
|
||||
if (response.type === "loading") {
|
||||
throw new Error("Cannot transform loading responses")
|
||||
}
|
||||
|
||||
if (response.type === "network_fail") {
|
||||
throw new Error("Cannot transform failed responses")
|
||||
}
|
||||
|
||||
let body: any = new TextDecoder("utf-8").decode(response.body)
|
||||
|
||||
// Try parsing to JSON
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (_) {}
|
||||
|
||||
return {
|
||||
body,
|
||||
headers: response.headers,
|
||||
status: response.statusCode,
|
||||
|
||||
__newRes: response,
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
|
||||
export default function getEnvironmentVariablesFromScript(script: string) {
|
||||
const _variables: Record<string, string> = {}
|
||||
|
||||
const currentEnv = getCurrentEnvironment()
|
||||
|
||||
for (const variable of currentEnv.variables) {
|
||||
_variables[variable.key] = variable.value
|
||||
}
|
||||
|
||||
const globalEnv = getGlobalVariables()
|
||||
|
||||
if (globalEnv) {
|
||||
for (const variable of globalEnv) {
|
||||
_variables[variable.key] = variable.value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// the pw object is the proxy by which pre-request scripts can pass variables to the request.
|
||||
// for security and control purposes, this is the only way a pre-request script should modify variables.
|
||||
const pw = {
|
||||
environment: {
|
||||
set: (key: string, value: string) => (_variables[key] = value),
|
||||
},
|
||||
env: {
|
||||
set: (key: string, value: string) => (_variables[key] = value),
|
||||
},
|
||||
// globals that the script is allowed to have access to.
|
||||
}
|
||||
|
||||
// run pre-request script within this function so that it has access to the pw object.
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function("pw", script)(pw)
|
||||
} catch (_e) {}
|
||||
|
||||
return _variables
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export function getSourcePrefix(source) {
|
||||
const sourceEmojis = {
|
||||
// Source used for info messages.
|
||||
info: "\tℹ️ [INFO]:\t",
|
||||
// Source used for client to server messages.
|
||||
client: "\t⬅️ [SENT]:\t",
|
||||
// Source used for server to client messages.
|
||||
server: "\t➡️ [RECEIVED]:\t",
|
||||
}
|
||||
if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source]
|
||||
return ""
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<Nuxt />
|
||||
</template>
|
||||
23
netlify.toml
@@ -1,6 +1,17 @@
|
||||
[build.environment]
|
||||
NODE_VERSION = "14"
|
||||
NPM_FLAGS = "--prefix=/dev/null"
|
||||
|
||||
[build]
|
||||
functions = "netlify/"
|
||||
environment = { NODE_VERSION = "14" }
|
||||
base = "/"
|
||||
publish = "packages/hoppscotch-app/dist"
|
||||
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
|
||||
[[redirects]]
|
||||
from = "/discord"
|
||||
@@ -22,7 +33,7 @@
|
||||
|
||||
[[redirects]]
|
||||
from = "/careers"
|
||||
to = "https://www.notion.so/hoppscotch/3b9d5d5239a043bfb91701faabf5b8f0"
|
||||
to = "https://hoppscotch.notion.site/3b9d5d5239a043bfb91701faabf5b8f0"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
@@ -37,3 +48,9 @@
|
||||
to = "https://twitter.com/hoppscotch_io"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/github"
|
||||
to = "https://github.com/hoppscotch/hoppscotch"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method
|
||||
exports.handler = (event) => {
|
||||
switch (event.httpMethod) {
|
||||
case "GET":
|
||||
try {
|
||||
const name = event.queryStringParameters.name || "World"
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ message: `Hello ${name}` }),
|
||||
}
|
||||
} catch (e) {
|
||||
return { statusCode: 500, body: e.toString() }
|
||||
}
|
||||
|
||||
default:
|
||||
return { statusCode: 405, body: "Method Not Allowed" }
|
||||
}
|
||||
}
|
||||
63650
package-lock.json
generated
113
package.json
@@ -1,110 +1,29 @@
|
||||
{
|
||||
"name": "hoppscotch",
|
||||
"name": "hoppscotch-app",
|
||||
"version": "2.0.0",
|
||||
"description": "Open source API development ecosystem",
|
||||
"author": "Hoppscotch (support@hoppscotch.io)",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "vue-tsc --noEmit && nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate --modern",
|
||||
"analyze": "npx nuxt build -a",
|
||||
"lint:script": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint **/*.{css,scss,vue} --ignore-path .gitignore",
|
||||
"lint": "npm run lint:script && npm run lint:style",
|
||||
"lintfix": "eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix",
|
||||
"test": "jest",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky install",
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,js,vue}": "eslint",
|
||||
"*.{css,scss,vue}": "stylelint"
|
||||
"dev": "pnpm -r do-dev",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
"start": "pnpm -r do-prod-start",
|
||||
"lint": "pnpm -r do-lint",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
"pre-commit": "pnpm -r do-lint",
|
||||
"test": "pnpm -r do-test"
|
||||
},
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.10",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@nuxtjs/composition-api": "^0.28.0",
|
||||
"@nuxtjs/gtm": "^2.4.0",
|
||||
"@nuxtjs/i18n": "^7.0.3",
|
||||
"@nuxtjs/robots": "^2.5.0",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"@nuxtjs/toast": "^3.3.1",
|
||||
"ace-builds": "^1.4.12",
|
||||
"acorn": "^8.5.0",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"core-js": "^3.17.2",
|
||||
"esprima": "^4.0.1",
|
||||
"firebase": "^9.0.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-language-service-interface": "^2.8.4",
|
||||
"json-loader": "^0.5.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"node-interval-tree": "^1.3.3",
|
||||
"nuxt": "^2.15.8",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"rxjs": "^7.3.0",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"socketio-wildcard": "^2.0.0",
|
||||
"splitpanes": "^2.3.8",
|
||||
"tern": "^0.24.3",
|
||||
"vue-apollo": "^3.0.7",
|
||||
"vue-cli-plugin-apollo": "^0.22.2",
|
||||
"vue-functional-data-merge": "^3.1.0",
|
||||
"vue-github-button": "^1.3.0",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-tippy": "^4.11.0",
|
||||
"vuejs-auto-complete": "^0.9.0",
|
||||
"yargs-parser": "^20.2.9"
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^11.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.4",
|
||||
"@commitlint/cli": "^13.1.0",
|
||||
"@commitlint/config-conventional": "^13.1.0",
|
||||
"@nuxt/types": "^2.15.8",
|
||||
"@nuxt/typescript-build": "^2.1.0",
|
||||
"@nuxtjs/color-mode": "^2.1.1",
|
||||
"@nuxtjs/dotenv": "^1.4.1",
|
||||
"@nuxtjs/eslint-config-typescript": "^6.0.1",
|
||||
"@nuxtjs/google-analytics": "^2.4.0",
|
||||
"@nuxtjs/google-fonts": "^1.3.0",
|
||||
"@nuxtjs/pwa": "^3.3.5",
|
||||
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||
"@nuxtjs/svg": "^0.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@vue/runtime-dom": "^3.2.10",
|
||||
"@vue/test-utils": "^1.2.2",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^27.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-nuxt": ">=2.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"husky": "^7.0.2",
|
||||
"jest": "^27.1.0",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"lint-staged": "^11.1.2",
|
||||
"nuxt-windicss": "^1.2.3",
|
||||
"prettier": "^2.3.2",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.39.0",
|
||||
"sass-loader": "^10.2.0",
|
||||
"stylelint": "^13.12.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^4.2",
|
||||
"unplugin-vue2-script-setup": "^0.5.8",
|
||||
"vue-jest": "^3.0.7",
|
||||
"worker-loader": "^3.0.8"
|
||||
"@commitlint/cli": "^13.2.1",
|
||||
"@commitlint/config-conventional": "^13.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ module.exports = {
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:nuxt/recommended",
|
||||
],
|
||||
ignorePatterns: [
|
||||
"helpers/backend/graphql.ts"
|
||||
],
|
||||
plugins: ["vue", "prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
118
packages/hoppscotch-app/.gitignore
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
# Firebase
|
||||
.firebase
|
||||
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# Mac OSX
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
|
||||
# Build data
|
||||
.hoppscotch
|
||||
|
||||
# File explorer
|
||||
.directory
|
||||
|
||||
# Tests screenshots
|
||||
tests/*/screenshots
|
||||
|
||||
# Tests videos
|
||||
tests/*/videos
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# Andrew's crazy Volar shim generator
|
||||
shims-volar.d.ts
|
||||
|
||||
# Hoppscotch Backend Schema Introspection JSON
|
||||
helpers/backend/backend-schema.json
|
||||
|
||||
# GraphQL Type Generation
|
||||
helpers/backend/graphql.ts
|
||||
1
packages/hoppscotch-app/assets/icons/align-left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
|
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 291 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 306 B |
|
Before Width: | Height: | Size: 317 B After Width: | Height: | Size: 317 B |
1
packages/hoppscotch-app/assets/icons/box.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 435 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: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
1
packages/hoppscotch-app/assets/icons/clock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
1
packages/hoppscotch-app/assets/icons/columns.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
||||
<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"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>
|
||||
|
After Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 351 B |
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 429 B |
|
Before Width: | Height: | Size: 289 B After Width: | Height: | Size: 289 B |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 440 B |
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 453 B After Width: | Height: | Size: 453 B |
|
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 380 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 542 B After Width: | Height: | Size: 542 B |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |