Compare commits
115 Commits
refactor/t
...
feat/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da368a2d72 | ||
|
|
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 |
11
.github/workflows/tests.yml
vendored
@@ -21,5 +21,12 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- name: Cache .pnpm-store
|
||||||
- run: npm test
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.pnpm-store
|
||||||
|
key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
- name: Install pnpm
|
||||||
|
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||||
|
- name: Run tests
|
||||||
|
run: pnpm i && pnpm -r test
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -104,3 +104,9 @@ tests/*/screenshots
|
|||||||
|
|
||||||
# Tests videos
|
# Tests videos
|
||||||
tests/*/videos
|
tests/*/videos
|
||||||
|
|
||||||
|
# Local Netlify folder
|
||||||
|
.netlify
|
||||||
|
|
||||||
|
# Andrew's crazy Volar shim generator
|
||||||
|
shims-volar.d.ts
|
||||||
|
|||||||
16
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM node:12-alpine
|
FROM node:lts-alpine
|
||||||
|
|
||||||
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
|
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
|
||||||
|
|
||||||
@@ -9,17 +9,19 @@ RUN apk add --update --no-cache \
|
|||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
ADD . /app/
|
ADD . /app/
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
RUN pnpm i
|
||||||
|
|
||||||
ENV HOST 0.0.0.0
|
ENV HOST 0.0.0.0
|
||||||
EXPOSE 3000
|
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"]
|
||||||
|
|||||||
16
README.md
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://hoppscotch.io">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<img
|
||||||
src="https://raw.githubusercontent.com/hoppscotch/hoppscotch/main/static/logo.png"
|
src="https://avatars.githubusercontent.com/u/56705483"
|
||||||
alt="Hoppscotch Logo"
|
alt="Hoppscotch Logo"
|
||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
@@ -173,7 +173,7 @@ _Collections are synced with cloud / local session storage_
|
|||||||
|
|
||||||
- Hide your IP address
|
- Hide your IP address
|
||||||
- Fixes [`CORS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross Origin Resource Sharing) issues
|
- 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
|
- 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)**_
|
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/privacy)**_
|
||||||
@@ -290,7 +290,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
|
|
||||||
## **Developing**
|
## **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/.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)._
|
_Sample keys only works with the [production build](https://hoppscotch.io)._
|
||||||
|
|
||||||
@@ -302,8 +302,8 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
|
|||||||
### Local development environment
|
### Local development environment
|
||||||
|
|
||||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
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`).
|
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||||
3. Start the development server with `npm run dev`.
|
3. Start the development server with `pnpm run dev`.
|
||||||
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
||||||
|
|
||||||
### Docker compose
|
### Docker compose
|
||||||
@@ -323,9 +323,9 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
|
|||||||
## **Releasing**
|
## **Releasing**
|
||||||
|
|
||||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
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`).
|
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||||
3. Build the release files with `npm run generate`.
|
3. Build the release files with `pnpm run generate`.
|
||||||
4. Find the built project in `./dist`.
|
4. Find the built project in `packages/hoppscotch-app/dist`.
|
||||||
|
|
||||||
## **Contributing**
|
## **Contributing**
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,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"
|
"indexes": "firestore.indexes.json"
|
||||||
},
|
},
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"predeploy": ["mv .env.example .env && npm ci && npm run generate"],
|
"predeploy": [
|
||||||
"public": "dist",
|
"cd packages/hoppscotch-app && mv .env.example .env && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||||
|
],
|
||||||
|
"public": "packages/hoppscotch-app/dist",
|
||||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||||
"rewrites": [
|
"rewrites": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,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,6 +1,11 @@
|
|||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "14"
|
||||||
|
NPM_FLAGS = "--prefix=/dev/null"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
functions = "netlify/"
|
base = "/"
|
||||||
environment = { NODE_VERSION = "14" }
|
publish = "packages/hoppscotch-app/dist"
|
||||||
|
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
|
||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/discord"
|
from = "/discord"
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63638
package-lock.json
generated
109
package.json
@@ -1,110 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch",
|
"name": "hoppscotch-app",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Open source API development ecosystem",
|
"description": "Open source API development ecosystem",
|
||||||
"author": "Hoppscotch (support@hoppscotch.io)",
|
"author": "Hoppscotch (support@hoppscotch.io)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"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",
|
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"pre-commit": "lint-staged"
|
"dev": "pnpm -r do-dev",
|
||||||
},
|
"generate": "pnpm -r do-build-prod",
|
||||||
"lint-staged": {
|
"start": "pnpm -r do-prod-start",
|
||||||
"*.{ts,js,vue}": "eslint",
|
"lintfix": "pnpm -r do-lintfix",
|
||||||
"*.{css,scss,vue}": "stylelint"
|
"pre-commit": "pnpm -r do-lintfix"
|
||||||
},
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"./packages/*"
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.4.10",
|
"husky": "^7.0.2",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"lint-staged": "^11.1.2"
|
||||||
"@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",
|
|
||||||
"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",
|
|
||||||
"vue-toastification": "^1.7.11",
|
|
||||||
"vuejs-auto-complete": "^0.9.0",
|
|
||||||
"yargs-parser": "^20.2.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.15.5",
|
|
||||||
"@babel/preset-env": "^7.15.4",
|
|
||||||
"@commitlint/cli": "^13.1.0",
|
"@commitlint/cli": "^13.1.0",
|
||||||
"@commitlint/config-conventional": "^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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
packages/hoppscotch-app/.gitignore
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# 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
|
||||||
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 |
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 276 B |