Compare commits
13 Commits
hotfix/fet
...
chore/dock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9f8ed4e8 | ||
|
|
f8bbf320fb | ||
|
|
633d98bbbc | ||
|
|
44fabe6570 | ||
|
|
8acfe8afb0 | ||
|
|
e233f36ce0 | ||
|
|
e1cbe6e003 | ||
|
|
1c35ea6e65 | ||
|
|
6eb0426aca | ||
|
|
fc0c113e00 | ||
|
|
9e595ec594 | ||
|
|
1b1a09c675 | ||
|
|
6454d83486 |
@@ -7,103 +7,6 @@ services:
|
|||||||
# This service runs the backend app in the port 3170
|
# This service runs the backend app in the port 3170
|
||||||
hoppscotch-backend:
|
hoppscotch-backend:
|
||||||
container_name: hoppscotch-backend
|
container_name: hoppscotch-backend
|
||||||
build:
|
|
||||||
dockerfile: prod.Dockerfile
|
|
||||||
context: .
|
|
||||||
target: backend
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
|
||||||
- PORT=3170
|
|
||||||
volumes:
|
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
|
||||||
- /usr/src/app/node_modules/
|
|
||||||
depends_on:
|
|
||||||
hoppscotch-db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "3170:3170"
|
|
||||||
|
|
||||||
# The main hoppscotch app. This will be hosted at port 3000
|
|
||||||
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
|
||||||
# the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile
|
|
||||||
hoppscotch-app:
|
|
||||||
container_name: hoppscotch-app
|
|
||||||
build:
|
|
||||||
dockerfile: prod.Dockerfile
|
|
||||||
context: .
|
|
||||||
target: app
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
- hoppscotch-backend
|
|
||||||
ports:
|
|
||||||
- "3000:8080"
|
|
||||||
|
|
||||||
# The Self Host dashboard for managing the app. This will be hosted at port 3100
|
|
||||||
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
|
||||||
# the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile
|
|
||||||
hoppscotch-sh-admin:
|
|
||||||
container_name: hoppscotch-sh-admin
|
|
||||||
build:
|
|
||||||
dockerfile: prod.Dockerfile
|
|
||||||
context: .
|
|
||||||
target: sh_admin
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
- hoppscotch-backend
|
|
||||||
ports:
|
|
||||||
- "3100:8080"
|
|
||||||
|
|
||||||
# The service that spins up all 3 services at once in one container
|
|
||||||
hoppscotch-aio:
|
|
||||||
container_name: hoppscotch-aio
|
|
||||||
build:
|
|
||||||
dockerfile: prod.Dockerfile
|
|
||||||
context: .
|
|
||||||
target: aio
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
hoppscotch-db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "3100:3100"
|
|
||||||
- "3170:3170"
|
|
||||||
|
|
||||||
# The preset DB service, you can delete/comment the below lines if
|
|
||||||
# you are using an external postgres instance
|
|
||||||
# This will be exposed at port 5432
|
|
||||||
hoppscotch-db:
|
|
||||||
image: postgres:15
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
user: postgres
|
|
||||||
environment:
|
|
||||||
# The default user defined by the docker image
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
# NOTE: Please UPDATE THIS PASSWORD!
|
|
||||||
POSTGRES_PASSWORD: testpass
|
|
||||||
POSTGRES_DB: hoppscotch
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
|
|
||||||
]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
# All the services listed below are deprececated
|
|
||||||
hoppscotch-old-backend:
|
|
||||||
container_name: hoppscotch-old-backend
|
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
@@ -125,26 +28,54 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3170:3000"
|
- "3170:3000"
|
||||||
|
|
||||||
hoppscotch-old-app:
|
# The main hoppscotch app. This will be hosted at port 3000
|
||||||
container_name: hoppscotch-old-app
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
|
# the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile
|
||||||
|
hoppscotch-app:
|
||||||
|
container_name: hoppscotch-app
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-old-backend
|
- hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3000:8080"
|
- "3000:8080"
|
||||||
|
|
||||||
hoppscotch-old-sh-admin:
|
# The Self Host dashboard for managing the app. This will be hosted at port 3100
|
||||||
container_name: hoppscotch-old-sh-admin
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
|
# the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile
|
||||||
|
hoppscotch-sh-admin:
|
||||||
|
container_name: hoppscotch-sh-admin
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-old-backend
|
- hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3100:8080"
|
- "3100:8080"
|
||||||
|
|
||||||
|
# The preset DB service, you can delete/comment the below lines if
|
||||||
|
# you are using an external postgres instance
|
||||||
|
# This will be exposed at port 5432
|
||||||
|
hoppscotch-db:
|
||||||
|
image: postgres:15
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
user: postgres
|
||||||
|
environment:
|
||||||
|
# The default user defined by the docker image
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
# NOTE: Please UPDATE THIS PASSWORD!
|
||||||
|
POSTGRES_PASSWORD: testpass
|
||||||
|
POSTGRES_DB: hoppscotch
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
curlCheck() {
|
|
||||||
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
|
|
||||||
echo "URL request failed!"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "URL request succeeded!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
curlCheck "http://localhost:3000"
|
|
||||||
curlCheck "http://localhost:3100"
|
|
||||||
curlCheck "http://localhost:3170/ping"
|
|
||||||
@@ -19,7 +19,7 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
|
|||||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app/app.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdOn: 'desc',
|
createdOn: 'desc',
|
||||||
},
|
},
|
||||||
skip: args.cursor ? 1 : 0,
|
skip: 1,
|
||||||
take: args.take,
|
take: args.take,
|
||||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ beforeEach(() => {
|
|||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
describe('UserHistoryService', () => {
|
describe('UserHistoryService', () => {
|
||||||
describe('fetchUserHistory', () => {
|
describe('fetchUserHistory', () => {
|
||||||
test('Should return a list of users REST history if exists', async () => {
|
test('Should return a list of users REST history if exists', async () => {
|
||||||
@@ -402,7 +400,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: date,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -412,7 +410,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: date,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-icon {
|
||||||
|
@apply transition;
|
||||||
|
@apply inline-flex;
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -326,7 +332,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e5cf"];
|
@apply after:content-["\e313"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,10 +487,6 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
|
||||||
@apply overscroll-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -572,11 +574,3 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gql-operation-not-highlight {
|
|
||||||
@apply opacity-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gql-operation-highlight {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -153,14 +153,13 @@
|
|||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
|
||||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
"context_menu": {
|
"context_menu": {
|
||||||
"set_environment_variable": "Set as variable",
|
"set_environment_variable": "Set as variable",
|
||||||
"add_parameters": "Add to parameters",
|
"add_parameter": "Add to parameter",
|
||||||
"open_request_in_new_tab": "Open request in new tab"
|
"open_link_in_new_tab": "Open link in new tab"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Header {count}",
|
"header": "Header {count}",
|
||||||
@@ -185,6 +184,7 @@
|
|||||||
"folder": "Folder is empty",
|
"folder": "Folder is empty",
|
||||||
"headers": "This request does not have any headers",
|
"headers": "This request does not have any headers",
|
||||||
"history": "History is empty",
|
"history": "History is empty",
|
||||||
|
"history_suggestions": "History does not have any matching entries",
|
||||||
"invites": "Invite list is empty",
|
"invites": "Invite list is empty",
|
||||||
"members": "Team is empty",
|
"members": "Team is empty",
|
||||||
"parameters": "This request does not have any parameters",
|
"parameters": "This request does not have any parameters",
|
||||||
@@ -194,6 +194,7 @@
|
|||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shortcodes": "Shortcodes are empty",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
|
"suggestions": "No matching suggestions found",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Team name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any teams",
|
||||||
"tests": "There are no tests for this request"
|
"tests": "There are no tests for this request"
|
||||||
@@ -280,10 +281,6 @@
|
|||||||
"graphql": {
|
"graphql": {
|
||||||
"mutations": "Mutations",
|
"mutations": "Mutations",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"switch_connection": "Switch connection",
|
|
||||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
|
||||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
|
||||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
|
||||||
"subscriptions": "Subscriptions"
|
"subscriptions": "Subscriptions"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
@@ -476,7 +473,6 @@
|
|||||||
"rename": "Rename Request",
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"stop": "Stop",
|
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
@@ -582,10 +578,6 @@
|
|||||||
"show_all": "Keyboard shortcuts",
|
"show_all": "Keyboard shortcuts",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
"others": {
|
|
||||||
"title": "Others",
|
|
||||||
"prettify": "Prettify Editor's Content"
|
|
||||||
},
|
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite people to Hoppscotch",
|
"invite": "Invite people to Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
@@ -606,9 +598,9 @@
|
|||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
"rename": "Rename Request",
|
"rename": "Rename Current Request",
|
||||||
"import_curl": "Import cURL",
|
"import_curl": "Import cURL",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "Show generated code",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"next_method": "Select Next method",
|
"next_method": "Select Next method",
|
||||||
"post_method": "Select POST method",
|
"post_method": "Select POST method",
|
||||||
@@ -649,11 +641,11 @@
|
|||||||
},
|
},
|
||||||
"spotlight": {
|
"spotlight": {
|
||||||
"general": {
|
"general": {
|
||||||
"help_menu": "Help and support",
|
"help_menu": "Open help and support menu",
|
||||||
"chat": "Chat with support",
|
"chat": "Chat with support",
|
||||||
"open_docs": "Read Documentation",
|
"open_docs": "Read Documentation",
|
||||||
"open_keybindings": "Keyboard shortcuts",
|
"open_keybindings": "Open keyboard shortcuts",
|
||||||
"social": "Social",
|
"social": "Social links and GitHub",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
@@ -661,18 +653,15 @@
|
|||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"switch_to": "Switch to",
|
"tab_parameters": "Open parameters tab",
|
||||||
"select_method": "Select method",
|
"tab_body": "Open body tab",
|
||||||
"save_as_new": "Save as new request",
|
"tab_headers": "Open headers tab",
|
||||||
"tab_parameters": "Parameters tab",
|
"tab_authorization": "Open authorization tab",
|
||||||
"tab_body": "Body tab",
|
"tab_pre_request_script": "Open pre-request script tab",
|
||||||
"tab_headers": "Headers tab",
|
"tab_tests": "Open tests tab"
|
||||||
"tab_authorization": "Authorization tab",
|
|
||||||
"tab_pre_request_script": "Pre-request script tab",
|
|
||||||
"tab_tests": "Tests tab"
|
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response",
|
"copy": "Copy response as JSON",
|
||||||
"download": "Download response as file",
|
"download": "Download response as file",
|
||||||
"title": "Response"
|
"title": "Response"
|
||||||
},
|
},
|
||||||
@@ -695,9 +684,8 @@
|
|||||||
"title": "Teams"
|
"title": "Teams"
|
||||||
},
|
},
|
||||||
"tab": {
|
"tab": {
|
||||||
"duplicate": "Duplicate tab",
|
|
||||||
"close_current": "Close current tab",
|
"close_current": "Close current tab",
|
||||||
"close_others": "Close other tabs",
|
"close_others": "Close others tab",
|
||||||
"new_tab": "Open a new tab",
|
"new_tab": "Open a new tab",
|
||||||
"title": "Tabs"
|
"title": "Tabs"
|
||||||
},
|
},
|
||||||
@@ -707,7 +695,9 @@
|
|||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
"interceptor": "Interceptor"
|
"interceptor": "Interceptor"
|
||||||
},
|
},
|
||||||
|
"change_interceptor": "Change Interceptor",
|
||||||
"change_language": "Change Language",
|
"change_language": "Change Language",
|
||||||
|
"install_extension": "Install Browser Extension",
|
||||||
"settings": {
|
"settings": {
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Black Mode",
|
"black": "Black Mode",
|
||||||
@@ -721,7 +711,8 @@
|
|||||||
"size_lg": "Change to Large"
|
"size_lg": "Change to Large"
|
||||||
},
|
},
|
||||||
"change_interceptor": "Change Interceptor",
|
"change_interceptor": "Change Interceptor",
|
||||||
"change_language": "Change Language"
|
"change_language": "Change Language",
|
||||||
|
"install_extension": "Install Browser Extension"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
|
|||||||
21
packages/hoppscotch-common/src/components.d.ts
vendored
21
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable */
|
// generated by unplugin-vue-components
|
||||||
/* prettier-ignore */
|
// We suggest you to commit this file into source control
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
@@ -29,7 +29,6 @@ declare module 'vue' {
|
|||||||
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
||||||
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
||||||
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
|
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
|
||||||
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
|
|
||||||
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
||||||
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
|
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
|
||||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||||
@@ -73,18 +72,12 @@ declare module 'vue' {
|
|||||||
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
||||||
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
||||||
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
||||||
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
|
|
||||||
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
|
|
||||||
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
||||||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||||
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
|
|
||||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||||
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
|
|
||||||
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
|
|
||||||
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
||||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||||
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
|
|
||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
@@ -111,7 +104,6 @@ declare module 'vue' {
|
|||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
|
||||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
@@ -144,6 +136,7 @@ declare module 'vue' {
|
|||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||||
|
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
@@ -157,7 +150,6 @@ declare module 'vue' {
|
|||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
@@ -222,4 +214,5 @@ declare module 'vue' {
|
|||||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||||
|
<AppSocial :show="showSocial" @hide-modal="showSocial = false" />
|
||||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
@@ -26,6 +27,7 @@ const t = useI18n()
|
|||||||
|
|
||||||
const showShortcuts = ref(false)
|
const showShortcuts = ref(false)
|
||||||
const showShare = ref(false)
|
const showShare = ref(false)
|
||||||
|
const showSocial = ref(false)
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
@@ -58,6 +60,10 @@ defineActionHandler("modals.share.toggle", () => {
|
|||||||
showShare.value = !showShare.value
|
showShare.value = !showShare.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineActionHandler("modals.social.toggle", () => {
|
||||||
|
showSocial.value = !showSocial.value
|
||||||
|
})
|
||||||
|
|
||||||
defineActionHandler("modals.login.toggle", () => {
|
defineActionHandler("modals.login.toggle", () => {
|
||||||
showLogin.value = !showLogin.value
|
showLogin.value = !showLogin.value
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -254,10 +254,8 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
|||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once the PWA code is initialized, this holds a method
|
* Once the PWA code is initialized, this holds a method
|
||||||
@@ -374,8 +372,6 @@ const handleTeamEdit = () => {
|
|||||||
editingTeamID.value = workspace.value.teamID
|
editingTeamID.value = workspace.value.teamID
|
||||||
editingTeamName.value = { name: selectedTeam.value.name }
|
editingTeamName.value = { name: selectedTeam.value.name }
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
} else {
|
|
||||||
noPermission()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +382,12 @@ const settings = ref<any | null>(null)
|
|||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
|
||||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
defineActionHandler("modals.team.edit", () => {
|
||||||
|
// TODO: Remove this hack
|
||||||
|
setTimeout(() => {
|
||||||
|
handleTeamEdit()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
defineActionHandler("modals.team.invite", () => {
|
defineActionHandler("modals.team.invite", () => {
|
||||||
if (
|
if (
|
||||||
@@ -394,8 +395,6 @@ defineActionHandler("modals.team.invite", () => {
|
|||||||
selectedTeam.value?.myRole === "EDITOR"
|
selectedTeam.value?.myRole === "EDITOR"
|
||||||
) {
|
) {
|
||||||
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
|
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
|
||||||
} else {
|
|
||||||
noPermission()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -406,8 +405,4 @@ defineActionHandler(
|
|||||||
},
|
},
|
||||||
computed(() => !currentUser.value)
|
computed(() => !currentUser.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
const noPermission = () => {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,18 +18,13 @@
|
|||||||
:horizontal="COLUMN_LAYOUT"
|
:horizontal="COLUMN_LAYOUT"
|
||||||
@resize="setPaneEvent($event, 'horizontal')"
|
@resize="setPaneEvent($event, 'horizontal')"
|
||||||
>
|
>
|
||||||
<Pane
|
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
|
||||||
:size="PANE_MAIN_TOP_SIZE"
|
|
||||||
class="flex flex-col !overflow-auto"
|
|
||||||
min-size="25"
|
|
||||||
>
|
|
||||||
<slot name="primary" />
|
<slot name="primary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane
|
<Pane
|
||||||
v-if="hasSecondary"
|
v-if="hasSecondary"
|
||||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||||
class="flex flex-col !overflow-auto"
|
class="flex flex-col !overflow-auto"
|
||||||
min-size="25"
|
|
||||||
>
|
>
|
||||||
<slot name="secondary" />
|
<slot name="secondary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
@@ -38,7 +33,7 @@
|
|||||||
<Pane
|
<Pane
|
||||||
v-if="SIDEBAR && hasSidebar"
|
v-if="SIDEBAR && hasSidebar"
|
||||||
:size="PANE_SIDEBAR_SIZE"
|
:size="PANE_SIDEBAR_SIZE"
|
||||||
min-size="25"
|
min-size="20"
|
||||||
class="flex flex-col !overflow-auto bg-primaryContrast"
|
class="flex flex-col !overflow-auto bg-primaryContrast"
|
||||||
>
|
>
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
@@ -83,10 +78,10 @@ type PaneEvent = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANE_MAIN_SIZE = ref(70)
|
const PANE_MAIN_SIZE = ref(74)
|
||||||
const PANE_SIDEBAR_SIZE = ref(30)
|
const PANE_SIDEBAR_SIZE = ref(26)
|
||||||
const PANE_MAIN_TOP_SIZE = ref(35)
|
const PANE_MAIN_TOP_SIZE = ref(42)
|
||||||
const PANE_MAIN_BOTTOM_SIZE = ref(65)
|
const PANE_MAIN_BOTTOM_SIZE = ref(58)
|
||||||
|
|
||||||
if (!COLUMN_LAYOUT.value) {
|
if (!COLUMN_LAYOUT.value) {
|
||||||
PANE_MAIN_TOP_SIZE.value = 50
|
PANE_MAIN_TOP_SIZE.value = 50
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||||
<div class="flex mb-4 space-x-2">
|
<div class="flex pb-4 my-4 space-x-2">
|
||||||
<div class="flex flex-col items-end space-y-4 text-right">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex items-center flex-1">
|
||||||
{{ t("shortcut.request.send_request") }}
|
{{ t("shortcut.request.send_request") }}
|
||||||
|
|||||||
135
packages/hoppscotch-common/src/components/app/Social.vue
Normal file
135
packages/hoppscotch-common/src/components/app/Social.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
v-if="show"
|
||||||
|
dialog
|
||||||
|
:title="t('app.social_links')"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<a
|
||||||
|
v-for="(platform, index) in platforms"
|
||||||
|
:key="`platform-${index}`"
|
||||||
|
:href="platform.link"
|
||||||
|
target="_blank"
|
||||||
|
class="social-link"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<component :is="platform.icon" class="w-6 h-6" />
|
||||||
|
<span class="mt-3">
|
||||||
|
{{ platform.name }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button class="social-link" @click="copyAppLink">
|
||||||
|
<component :is="copyIcon" class="w-6 h-6 text-xl" />
|
||||||
|
<span class="mt-3">
|
||||||
|
{{ t("app.copy") }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<p class="text-secondaryLight">
|
||||||
|
{{ t("app.social_description") }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useToast } from "@composables/toast"
|
||||||
|
import { refAutoReset } from "@vueuse/core"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import IconFacebook from "~icons/brands/facebook"
|
||||||
|
import IconLinkedIn from "~icons/brands/linkedin"
|
||||||
|
import IconReddit from "~icons/brands/reddit"
|
||||||
|
import IconTwitter from "~icons/brands/twitter"
|
||||||
|
import IconCheck from "~icons/lucide/check"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconGitHub from "~icons/lucide/github"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const url = "https://hoppscotch.io"
|
||||||
|
|
||||||
|
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
|
IconCopy,
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
|
||||||
|
const platforms = [
|
||||||
|
{
|
||||||
|
name: "GitHub",
|
||||||
|
icon: IconGitHub,
|
||||||
|
link: `https://hoppscotch.io/github`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Twitter",
|
||||||
|
icon: IconTwitter,
|
||||||
|
link: `https://twitter.com/hoppscotch_io`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Facebook",
|
||||||
|
icon: IconFacebook,
|
||||||
|
link: `https://www.facebook.com/hoppscotch.io`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reddit",
|
||||||
|
icon: IconReddit,
|
||||||
|
link: `https://www.reddit.com/r/hoppscotch`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkedIn",
|
||||||
|
icon: IconLinkedIn,
|
||||||
|
link: `https://www.linkedin.com/company/hoppscotch/`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const copyAppLink = () => {
|
||||||
|
copyToClipboard(url)
|
||||||
|
copyIcon.value = IconCheck
|
||||||
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.social-link {
|
||||||
|
@apply border border-dividerLight;
|
||||||
|
@apply rounded;
|
||||||
|
@apply flex-col flex;
|
||||||
|
@apply p-4;
|
||||||
|
@apply items-center;
|
||||||
|
@apply justify-center;
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply hover: (bg-primaryLight text-secondaryDark);
|
||||||
|
@apply focus: outline-none;
|
||||||
|
@apply focus-visible: border-divider;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
@apply opacity-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<IconLucideCheckCircle class="text-accent" />
|
|
||||||
</template>
|
|
||||||
@@ -111,7 +111,6 @@ import {
|
|||||||
SwitchWorkspaceSpotlightSearcherService,
|
SwitchWorkspaceSpotlightSearcherService,
|
||||||
WorkspaceSpotlightSearcherService,
|
WorkspaceSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -139,7 +138,6 @@ useService(EnvironmentsSpotlightSearcherService)
|
|||||||
useService(SwitchEnvSpotlightSearcherService)
|
useService(SwitchEnvSpotlightSearcherService)
|
||||||
useService(WorkspaceSpotlightSearcherService)
|
useService(WorkspaceSpotlightSearcherService)
|
||||||
useService(SwitchWorkspaceSpotlightSearcherService)
|
useService(SwitchWorkspaceSpotlightSearcherService)
|
||||||
useService(InterceptorSpotlightSearcherService)
|
|
||||||
|
|
||||||
const search = ref("")
|
const search = ref("")
|
||||||
|
|
||||||
@@ -266,3 +264,4 @@ function newUseArrowKeysForNavigation() {
|
|||||||
return { selectedEntry }
|
return { selectedEntry }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
~/services/spotlight/searchers/workspace.searcher
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
updateTeamRequest,
|
updateTeamRequest,
|
||||||
} from "~/helpers/backend/mutations/TeamRequest"
|
} from "~/helpers/backend/mutations/TeamRequest"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
|
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import {
|
import {
|
||||||
@@ -81,9 +82,8 @@ import {
|
|||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { computedWithControl } from "@vueuse/core"
|
import { computedWithControl } from "@vueuse/core"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
|
|
||||||
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -122,14 +122,10 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gqlRequestName = computedWithControl(
|
const gqlRequestName = useGQLRequestName()
|
||||||
() => activeGQLTab.value,
|
|
||||||
() => activeGQLTab.value.document.request.name
|
|
||||||
)
|
|
||||||
|
|
||||||
const restRequestName = computedWithControl(
|
const restRequestName = computedWithControl(
|
||||||
() => activeRESTTab.value,
|
() => currentActiveTab.value,
|
||||||
() => activeRESTTab.value.document.request.name
|
() => currentActiveTab.value.document.request.name
|
||||||
)
|
)
|
||||||
|
|
||||||
const reqName = computed(() => {
|
const reqName = computed(() => {
|
||||||
@@ -145,13 +141,11 @@ const reqName = computed(() => {
|
|||||||
const requestName = ref(reqName.value)
|
const requestName = ref(reqName.value)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [activeRESTTab.value, activeGQLTab.value],
|
() => [currentActiveTab.value, gqlRequestName.value],
|
||||||
() => {
|
() => {
|
||||||
if (props.mode === "rest") {
|
if (props.mode === "rest") {
|
||||||
requestName.value = activeRESTTab.value?.document.request.name ?? ""
|
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
||||||
} else {
|
} else requestName.value = gqlRequestName.value
|
||||||
requestName.value = activeGQLTab.value?.document.request.name ?? ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -208,10 +202,15 @@ const saveRequestAs = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUpdated =
|
let requestUpdated
|
||||||
props.mode === "rest"
|
|
||||||
? cloneDeep(activeRESTTab.value.document.request)
|
if (props.request) {
|
||||||
: cloneDeep(activeGQLTab.value.document.request)
|
requestUpdated = cloneDeep(props.request)
|
||||||
|
} else if (props.mode === "rest") {
|
||||||
|
requestUpdated = cloneDeep(currentActiveTab.value.document.request)
|
||||||
|
} else {
|
||||||
|
requestUpdated = cloneDeep(getGQLSession().request)
|
||||||
|
}
|
||||||
|
|
||||||
requestUpdated.name = requestName.value
|
requestUpdated.name = requestName.value
|
||||||
|
|
||||||
@@ -224,7 +223,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -251,7 +250,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -279,7 +278,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -439,7 +438,7 @@ const updateTeamCollectionOrFolder = (
|
|||||||
(result) => {
|
(result) => {
|
||||||
const { createRequestInCollection } = result
|
const { createRequestInCollection } = result
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -460,7 +459,7 @@ const updateTeamCollectionOrFolder = (
|
|||||||
const requestSaved = () => {
|
const requestSaved = () => {
|
||||||
toast.success(`${t("request.added")}`)
|
toast.success(`${t("request.added")}`)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
activeRESTTab.value.document.isDirty = false
|
currentActiveTab.value.document.isDirty = false
|
||||||
})
|
})
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
import { getGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -63,7 +63,7 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = currentActiveTab.value?.document.request.name
|
editingName.value = getGQLSession().request.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
@click="
|
@click="
|
||||||
emit('add-request', {
|
emit('add-request', {
|
||||||
path: `${collectionIndex}`,
|
path: `${collectionIndex}`,
|
||||||
index: collection.requests.length,
|
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -220,7 +219,6 @@ import {
|
|||||||
moveGraphqlRequest,
|
moveGraphqlRequest,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
picked: { type: Object, default: null },
|
picked: { type: Object, default: null },
|
||||||
@@ -295,22 +293,6 @@ const removeCollection = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleTabs = getTabsRefTo((tab) => {
|
|
||||||
const ctx = tab.document.saveContext
|
|
||||||
|
|
||||||
if (!ctx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
ctx.originLocation === "user-collection" &&
|
|
||||||
ctx.folderPath.startsWith(props.collectionIndex.toString())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const tab of possibleTabs) {
|
|
||||||
tab.value.document.saveContext = undefined
|
|
||||||
tab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,7 @@
|
|||||||
:icon="IconFilePlus"
|
:icon="IconFilePlus"
|
||||||
:title="t('request.new')"
|
:title="t('request.new')"
|
||||||
class="hidden group-hover:inline-flex"
|
class="hidden group-hover:inline-flex"
|
||||||
@click="
|
@click="emit('add-request', { path: folderPath })"
|
||||||
emit('add-request', {
|
|
||||||
path: folderPath,
|
|
||||||
index: folder.requests.length,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -203,7 +198,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -255,8 +249,10 @@ const collectionIcon = computed(() => {
|
|||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
emit("select", {
|
emit("select", {
|
||||||
pickedType: "gql-my-folder",
|
picked: {
|
||||||
folderPath: props.folderPath,
|
pickedType: "gql-my-folder",
|
||||||
|
folderPath: props.folderPath,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,22 +273,6 @@ const removeFolder = () => {
|
|||||||
emit("select", { picked: null })
|
emit("select", { picked: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleTabs = getTabsRefTo((tab) => {
|
|
||||||
const ctx = tab.document.saveContext
|
|
||||||
|
|
||||||
if (!ctx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
ctx.originLocation === "user-collection" &&
|
|
||||||
ctx.folderPath.startsWith(props.folderPath)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const tab of possibleTabs) {
|
|
||||||
tab.value.document.saveContext = undefined
|
|
||||||
tab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlFolder(props.folderPath, props.folder.id)
|
removeGraphqlFolder(props.folderPath, props.folder.id)
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,22 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
{{ request.name }}
|
{{ request.name }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
v-if="isActive"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
|
||||||
:title="`${t('collection.request_in_use')}`"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="!saveRequest"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconRotateCCW"
|
||||||
|
:title="t('action.restore')"
|
||||||
|
class="hidden group-hover:inline-flex"
|
||||||
|
@click="selectRequest()"
|
||||||
|
/>
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
ref="options"
|
ref="options"
|
||||||
@@ -127,6 +121,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
import IconFile from "~icons/lucide/file"
|
import IconFile from "~icons/lucide/file"
|
||||||
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
@@ -137,12 +132,7 @@ import { useToast } from "@composables/toast"
|
|||||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||||
import {
|
import { setGQLSession } from "~/newstore/GQLSession"
|
||||||
createNewTab,
|
|
||||||
getTabRefWithSaveContext,
|
|
||||||
currentTabID,
|
|
||||||
currentActiveTab,
|
|
||||||
} from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
@@ -164,18 +154,6 @@ const props = defineProps({
|
|||||||
requestIndex: { type: Number, default: null },
|
requestIndex: { type: Number, default: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
const isActive = computed(() => {
|
|
||||||
const saveCtx = currentActiveTab.value?.document.saveContext
|
|
||||||
|
|
||||||
if (!saveCtx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
saveCtx.originLocation === "user-collection" &&
|
|
||||||
saveCtx.folderPath === props.folderPath &&
|
|
||||||
saveCtx.requestIndex === props.requestIndex
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Better types please
|
// TODO: Better types please
|
||||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||||
|
|
||||||
@@ -201,24 +179,7 @@ const selectRequest = () => {
|
|||||||
if (props.saveRequest) {
|
if (props.saveRequest) {
|
||||||
pick()
|
pick()
|
||||||
} else {
|
} else {
|
||||||
const possibleTab = getTabRefWithSaveContext({
|
setGQLSession({
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Switch to that request if that request is open
|
|
||||||
if (possibleTab) {
|
|
||||||
currentTabID.value = possibleTab.value.id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewTab({
|
|
||||||
saveContext: {
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
},
|
|
||||||
request: cloneDeep(
|
request: cloneDeep(
|
||||||
makeGQLRequest({
|
makeGQLRequest({
|
||||||
name: props.request.name,
|
name: props.request.name,
|
||||||
@@ -229,7 +190,8 @@ const selectRequest = () => {
|
|||||||
auth: props.request.auth,
|
auth: props.request.auth,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
isDirty: false,
|
schema: "",
|
||||||
|
response: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,18 +214,6 @@ const removeRequest = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach the request from any of the tabs
|
|
||||||
const possibleTab = getTabRefWithSaveContext({
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (possibleTab) {
|
|
||||||
possibleTab.value.document.saveContext = undefined
|
|
||||||
possibleTab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ import {
|
|||||||
addGraphqlFolder,
|
addGraphqlFolder,
|
||||||
saveGraphqlRequestAs,
|
saveGraphqlRequestAs,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
|
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
@@ -145,7 +146,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -265,22 +265,17 @@ export default defineComponent({
|
|||||||
this.$data.editingCollectionIndex = collectionIndex
|
this.$data.editingCollectionIndex = collectionIndex
|
||||||
this.displayModalEdit(true)
|
this.displayModalEdit(true)
|
||||||
},
|
},
|
||||||
onAddRequest({ name, path, index }) {
|
onAddRequest({ name, path }) {
|
||||||
const newRequest = {
|
const newRequest = {
|
||||||
...currentActiveTab.value.document.request,
|
...getGQLSession().request,
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
|
|
||||||
saveGraphqlRequestAs(path, newRequest)
|
saveGraphqlRequestAs(path, newRequest)
|
||||||
|
setGQLSession({
|
||||||
createNewTab({
|
|
||||||
saveContext: {
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: path,
|
|
||||||
requestIndex: index,
|
|
||||||
},
|
|
||||||
request: newRequest,
|
request: newRequest,
|
||||||
isDirty: false,
|
schema: "",
|
||||||
|
response: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
>
|
>
|
||||||
<WorkspaceCurrent :section="t('tab.collections')" />
|
<WorkspaceCurrent :section="t('tab.collections')" />
|
||||||
|
|
||||||
<HoppSmartInput
|
<input
|
||||||
v-model="filterTexts"
|
v-model="filterTexts"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
|
class="py-2 pl-4 pr-2 bg-transparent !border-0"
|
||||||
type="search"
|
type="search"
|
||||||
:autofocus="false"
|
|
||||||
:disabled="collectionsType.type === 'team-collections'"
|
:disabled="collectionsType.type === 'team-collections'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +238,6 @@ import {
|
|||||||
resetTeamRequestsContext,
|
resetTeamRequestsContext,
|
||||||
} from "~/helpers/collection/collection"
|
} from "~/helpers/collection/collection"
|
||||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -2068,8 +2066,4 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("collection.new", () => {
|
|
||||||
displayModalAdd(true)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,13 +34,6 @@
|
|||||||
@hide-modal="displayModalNew(false)"
|
@hide-modal="displayModalNew(false)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
|
||||||
:show="showConfirmRemoveEnvModal"
|
|
||||||
:title="t('confirm.remove_team')"
|
|
||||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
|
||||||
@resolve="removeSelectedEnvironment()"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -51,7 +44,6 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
|||||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import {
|
import {
|
||||||
getSelectedEnvironmentIndex,
|
|
||||||
globalEnv$,
|
globalEnv$,
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
@@ -62,15 +54,8 @@ import { workspaceStatus$ } from "~/newstore/workspace"
|
|||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||||
import { useLocalState } from "~/newstore/localstate"
|
import { useLocalState } from "~/newstore/localstate"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
|
||||||
import { deleteEnvironment } from "~/newstore/environments"
|
|
||||||
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
type EnvironmentType = "my-environments" | "team-environments"
|
type EnvironmentType = "my-environments" | "team-environments"
|
||||||
|
|
||||||
@@ -183,7 +168,6 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const showConfirmRemoveEnvModal = ref(false)
|
|
||||||
const showModalNew = ref(false)
|
const showModalNew = ref(false)
|
||||||
const showModalDetails = ref(false)
|
const showModalDetails = ref(false)
|
||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
@@ -210,30 +194,6 @@ const editEnvironment = (environmentIndex: "Global") => {
|
|||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSelectedEnvironment = () => {
|
|
||||||
const selectedEnvIndex = getSelectedEnvironmentIndex()
|
|
||||||
if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return
|
|
||||||
|
|
||||||
if (selectedEnvIndex?.type === "MY_ENV") {
|
|
||||||
deleteEnvironment(selectedEnvIndex.index)
|
|
||||||
toast.success(`${t("state.deleted")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEnvIndex?.type === "TEAM_ENV") {
|
|
||||||
pipe(
|
|
||||||
deleteTeamEnvironment(selectedEnvIndex.teamEnvID),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
toast.success(`${t("team_environment.deleted")}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
}
|
}
|
||||||
@@ -243,10 +203,6 @@ defineActionHandler("modals.environment.new", () => {
|
|||||||
showModalDetails.value = true
|
showModalDetails.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("modals.environment.delete-selected", () => {
|
|
||||||
showConfirmRemoveEnvModal.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.my.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName }) => {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
|
class="mb-4"
|
||||||
@click="addEnvironmentVariable"
|
@click="addEnvironmentVariable"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
outline
|
outline
|
||||||
|
class="mb-4"
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -86,11 +86,13 @@
|
|||||||
disabled
|
disabled
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-else
|
v-else
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
|
class="mb-4"
|
||||||
@click="addEnvironmentVariable"
|
@click="addEnvironmentVariable"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
disabled
|
disabled
|
||||||
filled
|
filled
|
||||||
|
class="mb-4"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
:title="t('team.no_access')"
|
:title="t('team.no_access')"
|
||||||
:label="t('action.new')"
|
:label="t('action.new')"
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
outline
|
outline
|
||||||
|
class="mb-4"
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -312,10 +312,8 @@ const authProviders: AuthProviderItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
|
const allowedAuthProvidersIDsString: string | undefined = import.meta.env
|
||||||
// prettier-ignore
|
.VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
const allowedAuthProvidersIDsString =
|
|
||||||
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
|
|
||||||
|
|
||||||
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
|
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
|
||||||
? allowedAuthProvidersIDsString.split(",")
|
? allowedAuthProvidersIDsString.split(",")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div
|
<div
|
||||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||||
>
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="font-semibold truncate text-secondaryLight">
|
<label class="font-semibold truncate text-secondaryLight">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
:active="authName === 'None'"
|
:active="authName === 'None'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
auth.authType = 'none'
|
authType = 'none'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
:active="authName === 'Basic Auth'"
|
:active="authName === 'Basic Auth'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
auth.authType = 'basic'
|
authType = 'basic'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
:active="authName === 'Bearer'"
|
:active="authName === 'Bearer'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
auth.authType = 'bearer'
|
authType = 'bearer'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
:active="authName === 'OAuth 2.0'"
|
:active="authName === 'OAuth 2.0'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
auth.authType = 'oauth-2'
|
authType = 'oauth-2'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
:active="authName === 'API key'"
|
:active="authName === 'API key'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
auth.authType = 'api-key'
|
authType = 'api-key'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
:on="!URLExcludes.auth"
|
:on="!URLExcludes.auth"
|
||||||
@change="setExclude('auth', !$event)"
|
@change="setExclude('auth', !$event)"
|
||||||
>
|
>
|
||||||
{{ $t("authorization.include_in_url") }}
|
{{ t("authorization.include_in_url") }}
|
||||||
</HoppSmartCheckbox>-->
|
</HoppSmartCheckbox> -->
|
||||||
<HoppSmartCheckbox
|
<HoppSmartCheckbox
|
||||||
:on="authActive"
|
:on="authActive"
|
||||||
class="px-2"
|
class="px-2"
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="auth.authType === 'none'"
|
v-if="authType === 'none'"
|
||||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||||
:alt="`${t('empty.authorization')}`"
|
:alt="`${t('empty.authorization')}`"
|
||||||
:text="t('empty.authorization')"
|
:text="t('empty.authorization')"
|
||||||
@@ -127,47 +127,114 @@
|
|||||||
blank
|
blank
|
||||||
:icon="IconExternalLink"
|
:icon="IconExternalLink"
|
||||||
reverse
|
reverse
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="auth.authType === 'basic'">
|
<div v-if="authType === 'basic'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.username"
|
v-model="basicUsername"
|
||||||
:environment-highlights="false"
|
:environment-highlights="false"
|
||||||
:placeholder="t('authorization.username')"
|
:placeholder="t('authorization.username')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.password"
|
v-model="basicPassword"
|
||||||
:environment-highlights="false"
|
:environment-highlights="false"
|
||||||
:placeholder="t('authorization.password')"
|
:placeholder="t('authorization.password')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'bearer'">
|
<div v-if="authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.token"
|
v-model="bearerToken"
|
||||||
:environment-highlights="false"
|
:environment-highlights="false"
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'oauth-2'">
|
<div v-if="authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.token"
|
v-model="oauth2Token"
|
||||||
:environment-highlights="false"
|
:environment-highlights="false"
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HttpOAuth2Authorization v-model="auth" />
|
<HttpOAuth2Authorization />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'api-key'">
|
<div v-if="authType === 'api-key'">
|
||||||
<HttpAuthorizationApiKey v-model="auth" />
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="apiKey"
|
||||||
|
:environment-highlights="false"
|
||||||
|
placeholder="Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="apiValue"
|
||||||
|
:environment-highlights="false"
|
||||||
|
placeholder="Value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center border-b border-dividerLight">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<label class="ml-4 text-secondaryLight">
|
||||||
|
{{ t("authorization.pass_key_by") }}
|
||||||
|
</label>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => authTippyActions.focus()"
|
||||||
|
>
|
||||||
|
<span class="select-wrapper">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="addTo || t('state.none')"
|
||||||
|
class="pr-8 ml-2 rounded-none"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="authTippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||||
|
:active="addTo === 'Headers'"
|
||||||
|
:label="'Headers'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
addTo = 'Headers'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="
|
||||||
|
addTo === 'Query params' ? IconCircleDot : IconCircle
|
||||||
|
"
|
||||||
|
:active="addTo === 'Query params'"
|
||||||
|
:label="'Query params'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
addTo = 'Query params'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -190,45 +257,55 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import { computed, ref, Ref } from "vue"
|
||||||
|
import {
|
||||||
|
HoppGQLAuthAPIKey,
|
||||||
|
HoppGQLAuthBasic,
|
||||||
|
HoppGQLAuthBearer,
|
||||||
|
HoppGQLAuthOAuth2,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
|
||||||
|
import { pluckRef } from "@composables/ref"
|
||||||
|
import { useStream } from "@composables/stream"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
|
import { gqlAuth$, setGQLAuth } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import IconExternalLink from "~icons/lucide/external-link"
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { HoppGQLAuth } from "@hoppscotch/data"
|
|
||||||
import { pluckRef } from "@composables/ref"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { useVModel } from "@vueuse/core"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const props = defineProps<{
|
const auth = useStream(
|
||||||
modelValue: HoppGQLAuth
|
gqlAuth$,
|
||||||
}>()
|
{ authType: "none", authActive: true },
|
||||||
|
setGQLAuth
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", value: HoppGQLAuth): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const auth = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
const AUTH_KEY_NAME = {
|
|
||||||
basic: "Basic Auth",
|
|
||||||
bearer: "Bearer",
|
|
||||||
"oauth-2": "OAuth 2.0",
|
|
||||||
"api-key": "API key",
|
|
||||||
none: "None",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const authType = pluckRef(auth, "authType")
|
|
||||||
const authName = computed(() =>
|
|
||||||
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
|
||||||
)
|
)
|
||||||
|
const authType = pluckRef(auth, "authType")
|
||||||
|
const authName = computed(() => {
|
||||||
|
if (authType.value === "basic") return "Basic Auth"
|
||||||
|
else if (authType.value === "bearer") return "Bearer"
|
||||||
|
else if (authType.value === "oauth-2") return "OAuth 2.0"
|
||||||
|
else if (authType.value === "api-key") return "API key"
|
||||||
|
else return "None"
|
||||||
|
})
|
||||||
const authActive = pluckRef(auth, "authActive")
|
const authActive = pluckRef(auth, "authActive")
|
||||||
|
const basicUsername = pluckRef(auth as Ref<HoppGQLAuthBasic>, "username")
|
||||||
|
const basicPassword = pluckRef(auth as Ref<HoppGQLAuthBasic>, "password")
|
||||||
|
const bearerToken = pluckRef(auth as Ref<HoppGQLAuthBearer>, "token")
|
||||||
|
const oauth2Token = pluckRef(auth as Ref<HoppGQLAuthOAuth2>, "token")
|
||||||
|
const apiKey = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "key")
|
||||||
|
const apiValue = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "value")
|
||||||
|
const addTo = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "addTo")
|
||||||
|
if (typeof addTo.value === "undefined") {
|
||||||
|
addTo.value = "Headers"
|
||||||
|
apiKey.value = ""
|
||||||
|
apiValue.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
@@ -239,4 +316,5 @@ const clearContent = () => {
|
|||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
|
const authTippyActions = ref<any | null>(null)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,430 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
|
||||||
>
|
|
||||||
<label class="font-semibold text-secondaryLight">
|
|
||||||
{{ t("tab.headers") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
|
||||||
blank
|
|
||||||
:title="t('app.wiki')"
|
|
||||||
:icon="IconHelpCircle"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.linewrap')"
|
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
|
||||||
:icon="IconWrapText"
|
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.bulk_mode')"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:class="{ '!text-accent': bulkMode }"
|
|
||||||
@click="bulkMode = !bulkMode"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('add.new')"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:disabled="bulkMode"
|
|
||||||
@click="addHeader"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
|
|
||||||
<div v-else>
|
|
||||||
<draggable
|
|
||||||
v-model="workingHeaders"
|
|
||||||
:item-key="(header: any) => `header-${header.id}`"
|
|
||||||
animation="250"
|
|
||||||
handle=".draggable-handle"
|
|
||||||
draggable=".draggable-content"
|
|
||||||
ghost-class="cursor-move"
|
|
||||||
chosen-class="bg-primaryLight"
|
|
||||||
drag-class="cursor-grabbing"
|
|
||||||
>
|
|
||||||
<template #item="{ element: header, index }">
|
|
||||||
<div
|
|
||||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
content:
|
|
||||||
index !== workingHeaders?.length - 1
|
|
||||||
? t('action.drag_to_reorder')
|
|
||||||
: null,
|
|
||||||
}"
|
|
||||||
:icon="IconGripVertical"
|
|
||||||
class="cursor-auto text-primary hover:text-primary"
|
|
||||||
:class="{
|
|
||||||
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
|
||||||
index !== workingHeaders?.length - 1,
|
|
||||||
}"
|
|
||||||
tabindex="-1"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<HoppSmartAutoComplete
|
|
||||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
|
||||||
:source="commonHeaders"
|
|
||||||
:spellcheck="false"
|
|
||||||
:value="header.key"
|
|
||||||
autofocus
|
|
||||||
styles="
|
|
||||||
bg-transparent
|
|
||||||
flex
|
|
||||||
flex-1
|
|
||||||
py-1
|
|
||||||
px-4
|
|
||||||
truncate
|
|
||||||
"
|
|
||||||
class="flex-1 !flex"
|
|
||||||
@input="
|
|
||||||
updateHeader(index, {
|
|
||||||
id: header.id,
|
|
||||||
key: $event,
|
|
||||||
value: header.value,
|
|
||||||
active: header.active,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:name="`value ${String(index)}`"
|
|
||||||
:value="header.value"
|
|
||||||
autofocus
|
|
||||||
@change="
|
|
||||||
updateHeader(index, {
|
|
||||||
id: header.id,
|
|
||||||
key: header.key,
|
|
||||||
value: ($event!.target! as HTMLInputElement).value,
|
|
||||||
active: header.active,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
header.hasOwnProperty('active')
|
|
||||||
? header.active
|
|
||||||
? t('action.turn_off')
|
|
||||||
: t('action.turn_on')
|
|
||||||
: t('action.turn_off')
|
|
||||||
"
|
|
||||||
:icon="
|
|
||||||
header.hasOwnProperty('active')
|
|
||||||
? header.active
|
|
||||||
? IconCheckCircle
|
|
||||||
: IconCircle
|
|
||||||
: IconCheckCircle
|
|
||||||
"
|
|
||||||
color="green"
|
|
||||||
@click="
|
|
||||||
updateHeader(index, {
|
|
||||||
id: header.id,
|
|
||||||
key: header.key,
|
|
||||||
value: header.value,
|
|
||||||
active: !header.active,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="deleteHeader(index)"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="workingHeaders.length === 0"
|
|
||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
|
||||||
:alt="`${t('empty.headers')}`"
|
|
||||||
:text="t('empty.headers')"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="`${t('add.new')}`"
|
|
||||||
filled
|
|
||||||
:icon="IconPlus"
|
|
||||||
@click="addHeader"
|
|
||||||
/>
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconPlus from "~icons/lucide/plus"
|
|
||||||
import IconGripVertical from "~icons/lucide/grip-vertical"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconTrash from "~icons/lucide/trash"
|
|
||||||
import IconCircle from "~icons/lucide/circle"
|
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
|
||||||
import { reactive, ref, watch } from "vue"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import * as A from "fp-ts/Array"
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray"
|
|
||||||
import { pipe, flow } from "fp-ts/function"
|
|
||||||
import {
|
|
||||||
GQLHeader,
|
|
||||||
rawKeyValueEntriesToString,
|
|
||||||
parseRawKeyValueEntriesE,
|
|
||||||
RawKeyValueEntry,
|
|
||||||
HoppGQLRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import draggable from "vuedraggable-es"
|
|
||||||
import { clone, cloneDeep, isEqual } from "lodash-es"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { commonHeaders } from "~/helpers/headers"
|
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
|
||||||
import { useVModel } from "@vueuse/core"
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
// v-model integration with props and emit
|
|
||||||
const props = defineProps<{ modelValue: HoppGQLRequest }>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", value: HoppGQLRequest): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const request = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
const idTicker = ref(0)
|
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
|
||||||
const bulkMode = ref(false)
|
|
||||||
const bulkHeaders = ref("")
|
|
||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
|
||||||
|
|
||||||
const bulkEditor = ref<any | null>(null)
|
|
||||||
|
|
||||||
useCodemirror(
|
|
||||||
bulkEditor,
|
|
||||||
bulkHeaders,
|
|
||||||
reactive({
|
|
||||||
extendedEditorConfig: {
|
|
||||||
mode: "text/x-yaml",
|
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
|
||||||
lineWrapping: linewrapEnabled,
|
|
||||||
},
|
|
||||||
linter: null,
|
|
||||||
completer: null,
|
|
||||||
environmentHighlights: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// The UI representation of the headers list (has the empty end header)
|
|
||||||
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
|
|
||||||
{
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// Rule: Working Headers always have one empty header or the last element is always an empty header
|
|
||||||
watch(workingHeaders, (headersList) => {
|
|
||||||
if (
|
|
||||||
headersList.length > 0 &&
|
|
||||||
headersList[headersList.length - 1].key !== ""
|
|
||||||
) {
|
|
||||||
workingHeaders.value.push({
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sync logic between headers and working headers
|
|
||||||
watch(
|
|
||||||
props.modelValue.headers,
|
|
||||||
(newHeadersList) => {
|
|
||||||
// Sync should overwrite working headers
|
|
||||||
const filteredWorkingHeaders = pipe(
|
|
||||||
workingHeaders.value,
|
|
||||||
A.filterMap(
|
|
||||||
flow(
|
|
||||||
O.fromPredicate((e) => e.key !== ""),
|
|
||||||
O.map(objRemoveKey("id"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredBulkHeaders = pipe(
|
|
||||||
parseRawKeyValueEntriesE(bulkHeaders.value),
|
|
||||||
E.map(
|
|
||||||
flow(
|
|
||||||
RA.filter((e) => e.key !== ""),
|
|
||||||
RA.toArray
|
|
||||||
)
|
|
||||||
),
|
|
||||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
|
|
||||||
workingHeaders.value = pipe(
|
|
||||||
newHeadersList,
|
|
||||||
A.map((x) => ({ id: idTicker.value++, ...x }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
|
|
||||||
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(workingHeaders, (newWorkingHeaders) => {
|
|
||||||
const fixedHeaders = pipe(
|
|
||||||
newWorkingHeaders,
|
|
||||||
A.filterMap(
|
|
||||||
flow(
|
|
||||||
O.fromPredicate((e) => e.key !== ""),
|
|
||||||
O.map(objRemoveKey("id"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(request.value.headers, fixedHeaders)) {
|
|
||||||
request.value.headers = cloneDeep(fixedHeaders)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bulk Editor Syncing with Working Headers
|
|
||||||
watch(bulkHeaders, (newBulkHeaders) => {
|
|
||||||
const filteredBulkHeaders = pipe(
|
|
||||||
parseRawKeyValueEntriesE(newBulkHeaders),
|
|
||||||
E.map(
|
|
||||||
flow(
|
|
||||||
RA.filter((e) => e.key !== ""),
|
|
||||||
RA.toArray
|
|
||||||
)
|
|
||||||
),
|
|
||||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(request.value.headers, filteredBulkHeaders)) {
|
|
||||||
request.value.headers = filteredBulkHeaders
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(workingHeaders, (newHeadersList) => {
|
|
||||||
// If we are in bulk mode, don't apply direct changes
|
|
||||||
if (bulkMode.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
|
|
||||||
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
|
|
||||||
value: item.substring(item.indexOf(":") + 1).trimLeft(),
|
|
||||||
active: !item.trim().startsWith("#"),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
|
|
||||||
|
|
||||||
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
|
|
||||||
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const addHeader = () => {
|
|
||||||
workingHeaders.value.push({
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
|
|
||||||
workingHeaders.value = workingHeaders.value.map((h, i) =>
|
|
||||||
i === index ? header : h
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteHeader = (index: number) => {
|
|
||||||
const headersBeforeDeletion = clone(workingHeaders.value)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
headersBeforeDeletion.length > 0 &&
|
|
||||||
index === headersBeforeDeletion.length - 1
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (deletionToast.value) {
|
|
||||||
deletionToast.value.goAway(0)
|
|
||||||
deletionToast.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
|
||||||
action: [
|
|
||||||
{
|
|
||||||
text: `${t("action.undo")}`,
|
|
||||||
onClick: (_: any, toastObject: any) => {
|
|
||||||
workingHeaders.value = headersBeforeDeletion
|
|
||||||
toastObject.goAway(0)
|
|
||||||
deletionToast.value = null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
onComplete: () => {
|
|
||||||
deletionToast.value = null
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
workingHeaders.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearContent = () => {
|
|
||||||
// set headers list to the initial state
|
|
||||||
workingHeaders.value = [
|
|
||||||
{
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
bulkHeaders.value = ""
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
|
||||||
>
|
|
||||||
<label class="font-semibold text-secondaryLight">
|
|
||||||
{{ t("request.query") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="subscriptionState === 'SUBSCRIBED'"
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
allowHTML: true,
|
|
||||||
}"
|
|
||||||
:title="`${t('request.stop')}`"
|
|
||||||
:label="`${t('request.stop')}`"
|
|
||||||
:icon="IconStop"
|
|
||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
|
||||||
@click="unsubscribe()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
allowHTML: true,
|
|
||||||
}"
|
|
||||||
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
|
||||||
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
|
|
||||||
:icon="IconPlay"
|
|
||||||
:disabled="!selectedOperation"
|
|
||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
|
||||||
@click="runQuery(selectedOperation)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
|
||||||
:title="`${t(
|
|
||||||
'request.save'
|
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
|
||||||
:label="`${t('request.save')}`"
|
|
||||||
:icon="IconSave"
|
|
||||||
class="rounded-none"
|
|
||||||
@click="saveRequest"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
|
||||||
blank
|
|
||||||
:title="t('app.wiki')"
|
|
||||||
:icon="IconHelpCircle"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
@click="clearGQLQuery()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.linewrap')"
|
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
|
||||||
:icon="IconWrapText"
|
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.prettify')"
|
|
||||||
:icon="prettifyQueryIcon"
|
|
||||||
@click="prettifyQuery"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.copy')"
|
|
||||||
:icon="copyQueryIcon"
|
|
||||||
@click="copyQuery"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref="queryEditor" class="flex flex-col flex-1"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconPlay from "~icons/lucide/play"
|
|
||||||
import IconStop from "~icons/lucide/stop-circle"
|
|
||||||
import IconSave from "~icons/lucide/save"
|
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
import IconInfo from "~icons/lucide/info"
|
|
||||||
import IconWand from "~icons/lucide/wand"
|
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
|
||||||
import { onMounted, reactive, ref, markRaw } from "vue"
|
|
||||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import * as gql from "graphql"
|
|
||||||
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
|
|
||||||
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
|
|
||||||
import { selectedGQLOpHighlight } from "~/helpers/editor/gql/operation"
|
|
||||||
import { debounce } from "lodash-es"
|
|
||||||
import { ViewUpdate } from "@codemirror/view"
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
import {
|
|
||||||
schema,
|
|
||||||
socketDisconnect,
|
|
||||||
subscriptionState,
|
|
||||||
} from "~/helpers/graphql/connection"
|
|
||||||
|
|
||||||
// Template refs
|
|
||||||
const queryEditor = ref<any | null>(null)
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "save-request"): void
|
|
||||||
(e: "update:modelValue", val: string): void
|
|
||||||
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|
||||||
IconCopy,
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
const prettifyQueryIcon = refAutoReset<
|
|
||||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
|
||||||
>(IconWand, 1000)
|
|
||||||
|
|
||||||
const linewrapEnabled = ref(true)
|
|
||||||
|
|
||||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
|
||||||
|
|
||||||
const gqlQueryString = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
|
|
||||||
if (!update.selectionSet) return
|
|
||||||
|
|
||||||
const selectedPos = update.state.selection.main.head
|
|
||||||
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const operations = gql.parse(queryString)
|
|
||||||
if (operations.definitions.length === 1) {
|
|
||||||
selectedOperation.value = operations
|
|
||||||
.definitions[0] as gql.OperationDefinitionNode
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedOperation.value =
|
|
||||||
(operations.definitions.find((def) => {
|
|
||||||
if (def.kind !== "OperationDefinition") return false
|
|
||||||
const { start, end } = def.loc!
|
|
||||||
return selectedPos >= start && selectedPos <= end
|
|
||||||
}) as gql.OperationDefinitionNode) ?? null
|
|
||||||
} catch (error) {
|
|
||||||
// console.error(error)
|
|
||||||
}
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
try {
|
|
||||||
const operations = gql.parse(gqlQueryString.value)
|
|
||||||
if (operations.definitions.length) {
|
|
||||||
selectedOperation.value = operations
|
|
||||||
.definitions[0] as gql.OperationDefinitionNode
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
useCodemirror(
|
|
||||||
queryEditor,
|
|
||||||
gqlQueryString,
|
|
||||||
reactive({
|
|
||||||
extendedEditorConfig: {
|
|
||||||
mode: "graphql",
|
|
||||||
placeholder: `${t("request.query")}`,
|
|
||||||
lineWrapping: linewrapEnabled,
|
|
||||||
},
|
|
||||||
linter: createGQLQueryLinter(schema),
|
|
||||||
completer: queryCompleter(schema),
|
|
||||||
environmentHighlights: false,
|
|
||||||
additionalExts: [markRaw(selectedGQLOpHighlight)],
|
|
||||||
onUpdate: debouncedOnUpdateQueryState,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// operations on graphql query string
|
|
||||||
// const operations = useReadonlyStream(props.request.operations$, [])
|
|
||||||
|
|
||||||
const prettifyQuery = () => {
|
|
||||||
try {
|
|
||||||
gqlQueryString.value = gql.print(
|
|
||||||
gql.parse(gqlQueryString.value, {
|
|
||||||
allowLegacyFragmentVariables: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
prettifyQueryIcon.value = IconCheck
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`${t("error.gql_prettify_invalid_query")}`)
|
|
||||||
prettifyQueryIcon.value = IconInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyQuery = () => {
|
|
||||||
copyToClipboard(gqlQueryString.value)
|
|
||||||
copyQueryIcon.value = IconCheck
|
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearGQLQuery = () => {
|
|
||||||
gqlQueryString.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
|
|
||||||
emit("run-query", definition)
|
|
||||||
}
|
|
||||||
const unsubscribe = () => {
|
|
||||||
socketDisconnect()
|
|
||||||
}
|
|
||||||
const saveRequest = () => {
|
|
||||||
emit("save-request")
|
|
||||||
}
|
|
||||||
|
|
||||||
defineActionHandler("editor.format", prettifyQuery)
|
|
||||||
</script>
|
|
||||||
@@ -17,127 +17,58 @@
|
|||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
id="get"
|
id="get"
|
||||||
name="get"
|
name="get"
|
||||||
:loading="connection.state === 'CONNECTING'"
|
:loading="isLoading"
|
||||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||||
class="w-32"
|
class="w-32"
|
||||||
@click="onConnectClick"
|
@click="onConnectClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartModal
|
|
||||||
v-if="connectionSwitchModal"
|
|
||||||
dialog
|
|
||||||
:dimissible="false"
|
|
||||||
:title="t('graphql.switch_connection')"
|
|
||||||
@close="connectionSwitchModal = false"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<p class="mb-4">
|
|
||||||
{{ t("graphql.connection_switch_url") }}:
|
|
||||||
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(0) }} </kbd>
|
|
||||||
</p>
|
|
||||||
<p class="mb-4">
|
|
||||||
{{ t("graphql.connection_switch_new_url") }}:
|
|
||||||
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(1) }} </kbd>
|
|
||||||
</p>
|
|
||||||
<p>{{ t("graphql.connection_switch_confirm") }}</p>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<span class="flex space-x-2">
|
|
||||||
<HoppButtonPrimary
|
|
||||||
:label="t('action.connect')"
|
|
||||||
:loading="connection.state === 'CONNECTING'"
|
|
||||||
outline
|
|
||||||
@click="switchConnection()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.cancel')"
|
|
||||||
outline
|
|
||||||
filled
|
|
||||||
@click="cancelSwitch()"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
|
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
import {
|
||||||
import { computed, ref, watch } from "vue"
|
gqlAuth$,
|
||||||
import { connection } from "~/helpers/graphql/connection"
|
gqlHeaders$,
|
||||||
import { connect } from "~/helpers/graphql/connection"
|
gqlURL$,
|
||||||
import { disconnect } from "~/helpers/graphql/connection"
|
setGQLURL,
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
} from "~/newstore/GQLSession"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const interceptorService = useService(InterceptorService)
|
const interceptorService = useService(InterceptorService)
|
||||||
|
|
||||||
const connectionSwitchModal = ref(false)
|
const props = defineProps<{
|
||||||
|
conn: GQLConnection
|
||||||
|
}>()
|
||||||
|
|
||||||
const connected = computed(() => connection.state === "CONNECTED")
|
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||||
|
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
|
||||||
const url = computed({
|
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||||
get: () => currentActiveTab.value?.document.request.url ?? "",
|
const auth = useReadonlyStream(gqlAuth$, {
|
||||||
set: (value) => {
|
authType: "none",
|
||||||
currentActiveTab.value!.document.request.url = value
|
authActive: true,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const url = useStream(gqlURL$, "", setGQLURL)
|
||||||
|
|
||||||
const onConnectClick = () => {
|
const onConnectClick = () => {
|
||||||
if (!connected.value) {
|
if (!connected.value) {
|
||||||
gqlConnect()
|
props.conn.connect(url.value, headers.value as any, auth.value)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_REQUEST_RUN",
|
||||||
|
platform: "graphql-schema",
|
||||||
|
strategy: interceptorService.currentInterceptorID.value!,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
props.conn.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gqlConnect = () => {
|
|
||||||
connect(url.value, currentActiveTab.value?.document.request.headers)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "graphql-schema",
|
|
||||||
strategy: interceptorService.currentInterceptorID.value!,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchConnection = () => {
|
|
||||||
gqlConnect()
|
|
||||||
connectionSwitchModal.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastTwoUrls = ref<string[]>([])
|
|
||||||
|
|
||||||
watch(
|
|
||||||
currentActiveTab,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
lastTwoUrls.value.push(newVal.document.request.url)
|
|
||||||
if (lastTwoUrls.value.length > 2) {
|
|
||||||
lastTwoUrls.value.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
connected.value &&
|
|
||||||
lastTwoUrls.value.length === 2 &&
|
|
||||||
lastTwoUrls.value.at(0) !== lastTwoUrls.value.at(1)
|
|
||||||
) {
|
|
||||||
disconnect()
|
|
||||||
connectionSwitchModal.value = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const cancelSwitch = () => {
|
|
||||||
if (connected.value) disconnect()
|
|
||||||
connectionSwitchModal.value = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,42 +2,311 @@
|
|||||||
<div class="flex flex-col flex-1 h-full">
|
<div class="flex flex-col flex-1 h-full">
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-model="selectedOptionTab"
|
v-model="selectedOptionTab"
|
||||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10"
|
||||||
:render-inactive-tabs="true"
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'query'"
|
:id="'query'"
|
||||||
:label="`${t('tab.query')}`"
|
:label="`${t('tab.query')}`"
|
||||||
:indicator="request.query && request.query.length > 0 ? true : false"
|
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
|
||||||
>
|
>
|
||||||
<GraphqlQuery
|
<div
|
||||||
v-model="request.query"
|
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery"
|
||||||
@run-query="runQuery"
|
>
|
||||||
@save-request="saveRequest"
|
<label class="font-semibold truncate text-secondaryLight">
|
||||||
/>
|
{{ t("request.query") }}
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||||
|
:title="`${t(
|
||||||
|
'request.run'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
|
:label="`${t('request.run')}`"
|
||||||
|
:icon="IconPlay"
|
||||||
|
class="rounded-none !text-accent !hover:text-accentDark"
|
||||||
|
@click="runQuery()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||||
|
:title="`${t(
|
||||||
|
'request.save'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||||
|
:label="`${t('request.save')}`"
|
||||||
|
:icon="IconSave"
|
||||||
|
class="rounded-none"
|
||||||
|
@click="saveRequest"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
@click="clearGQLQuery()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.linewrap')"
|
||||||
|
:class="{ '!text-accent': linewrapEnabledQuery }"
|
||||||
|
:icon="IconWrapText"
|
||||||
|
@click.prevent="linewrapEnabledQuery = !linewrapEnabledQuery"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.prettify')"
|
||||||
|
:icon="prettifyQueryIcon"
|
||||||
|
@click="prettifyQuery"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.copy')"
|
||||||
|
:icon="copyQueryIcon"
|
||||||
|
@click="copyQuery"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="queryEditor" class="flex flex-col flex-1"></div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'variables'"
|
:id="'variables'"
|
||||||
:label="`${t('tab.variables')}`"
|
:label="`${t('tab.variables')}`"
|
||||||
:indicator="
|
:indicator="variableString && variableString.length > 0 ? true : false"
|
||||||
request.variables && request.variables.length > 0 ? true : false
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<GraphqlVariable
|
<div
|
||||||
v-model="request.variables"
|
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||||
@run-query="runQuery"
|
>
|
||||||
@save-request="saveRequest"
|
<label class="font-semibold truncate text-secondaryLight">
|
||||||
/>
|
{{ t("request.variables") }}
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
@click="clearGQLVariables()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.linewrap')"
|
||||||
|
:class="{ '!text-accent': linewrapEnabledVariable }"
|
||||||
|
:icon="IconWrapText"
|
||||||
|
@click.prevent="
|
||||||
|
linewrapEnabledVariable = !linewrapEnabledVariable
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.prettify')"
|
||||||
|
:icon="prettifyVariablesIcon"
|
||||||
|
@click="prettifyVariableString"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.copy')"
|
||||||
|
:icon="copyVariablesIcon"
|
||||||
|
@click="copyVariables"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="variableEditor" class="flex flex-col flex-1"></div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'headers'"
|
:id="'headers'"
|
||||||
:label="`${t('tab.headers')}`"
|
:label="`${t('tab.headers')}`"
|
||||||
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
|
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
|
||||||
>
|
>
|
||||||
<GraphqlHeaders v-model="request" />
|
<div
|
||||||
|
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||||
|
>
|
||||||
|
<label class="font-semibold truncate text-secondaryLight">
|
||||||
|
{{ t("tab.headers") }}
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
@click="clearContent()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.linewrap')"
|
||||||
|
:class="{ '!text-accent': linewrapEnabled }"
|
||||||
|
:icon="IconWrapText"
|
||||||
|
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.bulk_mode')"
|
||||||
|
:icon="IconEdit"
|
||||||
|
:class="{ '!text-accent': bulkMode }"
|
||||||
|
@click="bulkMode = !bulkMode"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('add.new')"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:disabled="bulkMode"
|
||||||
|
@click="addHeader"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="bulkMode"
|
||||||
|
ref="bulkEditor"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
></div>
|
||||||
|
<div v-else>
|
||||||
|
<draggable
|
||||||
|
v-model="workingHeaders"
|
||||||
|
:item-key="(header) => `header-${header.id}`"
|
||||||
|
animation="250"
|
||||||
|
handle=".draggable-handle"
|
||||||
|
draggable=".draggable-content"
|
||||||
|
ghost-class="cursor-move"
|
||||||
|
chosen-class="bg-primaryLight"
|
||||||
|
drag-class="cursor-grabbing"
|
||||||
|
>
|
||||||
|
<template #item="{ element: header, index }">
|
||||||
|
<div
|
||||||
|
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{
|
||||||
|
theme: 'tooltip',
|
||||||
|
delay: [500, 20],
|
||||||
|
content:
|
||||||
|
index !== workingHeaders?.length - 1
|
||||||
|
? t('action.drag_to_reorder')
|
||||||
|
: null,
|
||||||
|
}"
|
||||||
|
:icon="IconGripVertical"
|
||||||
|
class="cursor-auto text-primary hover:text-primary"
|
||||||
|
:class="{
|
||||||
|
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
||||||
|
index !== workingHeaders?.length - 1,
|
||||||
|
}"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<HoppSmartAutoComplete
|
||||||
|
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||||
|
:source="commonHeaders"
|
||||||
|
:spellcheck="false"
|
||||||
|
:value="header.key"
|
||||||
|
autofocus
|
||||||
|
styles="
|
||||||
|
bg-transparent
|
||||||
|
flex
|
||||||
|
flex-1
|
||||||
|
py-1
|
||||||
|
px-4
|
||||||
|
truncate
|
||||||
|
"
|
||||||
|
class="flex-1 !flex"
|
||||||
|
@input="
|
||||||
|
updateHeader(index, {
|
||||||
|
id: header.id,
|
||||||
|
key: $event,
|
||||||
|
value: header.value,
|
||||||
|
active: header.active,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:name="`value ${String(index)}`"
|
||||||
|
:value="header.value"
|
||||||
|
autofocus
|
||||||
|
@change="
|
||||||
|
updateHeader(index, {
|
||||||
|
id: header.id,
|
||||||
|
key: header.key,
|
||||||
|
value: ($event!.target! as HTMLInputElement).value,
|
||||||
|
active: header.active,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="
|
||||||
|
header.hasOwnProperty('active')
|
||||||
|
? header.active
|
||||||
|
? t('action.turn_off')
|
||||||
|
: t('action.turn_on')
|
||||||
|
: t('action.turn_off')
|
||||||
|
"
|
||||||
|
:icon="
|
||||||
|
header.hasOwnProperty('active')
|
||||||
|
? header.active
|
||||||
|
? IconCheckCircle
|
||||||
|
: IconCircle
|
||||||
|
: IconCheckCircle
|
||||||
|
"
|
||||||
|
color="green"
|
||||||
|
@click="
|
||||||
|
updateHeader(index, {
|
||||||
|
id: header.id,
|
||||||
|
key: header.key,
|
||||||
|
value: header.value,
|
||||||
|
active: !header.active,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="deleteHeader(index)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="workingHeaders.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||||
|
:alt="`${t('empty.headers')}`"
|
||||||
|
:text="t('empty.headers')"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
class="mb-4"
|
||||||
|
@click="addHeader"
|
||||||
|
/>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
</div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||||
<GraphqlAuthorization v-model="request.auth" />
|
<GraphqlAuthorization />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
<CollectionsSaveRequest
|
<CollectionsSaveRequest
|
||||||
@@ -49,103 +318,432 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import IconPlay from "~icons/lucide/play"
|
||||||
|
import IconSave from "~icons/lucide/save"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconGripVertical from "~icons/lucide/grip-vertical"
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconTrash from "~icons/lucide/trash"
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconCheck from "~icons/lucide/check"
|
||||||
|
import IconInfo from "~icons/lucide/info"
|
||||||
|
import IconWand2 from "~icons/lucide/wand-2"
|
||||||
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
|
import { Ref, computed, reactive, ref, watch } from "vue"
|
||||||
|
import * as gql from "graphql"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import * as RA from "fp-ts/ReadonlyArray"
|
||||||
|
import { pipe, flow } from "fp-ts/function"
|
||||||
|
import {
|
||||||
|
GQLHeader,
|
||||||
|
makeGQLRequest,
|
||||||
|
rawKeyValueEntriesToString,
|
||||||
|
parseRawKeyValueEntriesE,
|
||||||
|
RawKeyValueEntry,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import draggable from "vuedraggable-es"
|
||||||
|
import { clone, cloneDeep, isEqual } from "lodash-es"
|
||||||
|
import { refAutoReset } from "@vueuse/core"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||||
import * as gql from "graphql"
|
|
||||||
import { clone } from "lodash-es"
|
|
||||||
import { computed, ref, watch } from "vue"
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
|
||||||
import { computedWithControl } from "@vueuse/core"
|
|
||||||
import {
|
import {
|
||||||
GQLResponseEvent,
|
gqlAuth$,
|
||||||
runGQLOperation,
|
gqlHeaders$,
|
||||||
gqlMessageEvent,
|
gqlQuery$,
|
||||||
} from "~/helpers/graphql/connection"
|
gqlResponse$,
|
||||||
|
gqlURL$,
|
||||||
|
gqlVariables$,
|
||||||
|
setGQLAuth,
|
||||||
|
setGQLHeaders,
|
||||||
|
setGQLQuery,
|
||||||
|
setGQLResponse,
|
||||||
|
setGQLVariables,
|
||||||
|
} from "~/newstore/GQLSession"
|
||||||
|
import { commonHeaders } from "~/helpers/headers"
|
||||||
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
|
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
|
import jsonLinter from "~/helpers/editor/linting/json"
|
||||||
|
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
|
||||||
|
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
|
||||||
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
import { editGraphqlRequest } from "~/newstore/collections"
|
|
||||||
|
|
||||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const selectedOptionTab = ref<OptionTabs>("query")
|
const selectedOptionTab = ref<OptionTabs>("query")
|
||||||
const interceptorService = useService(InterceptorService)
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
const interceptorService = useService(InterceptorService)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
conn: GQLConnection
|
||||||
|
}>()
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
// v-model integration with props and emit
|
const url = useReadonlyStream(gqlURL$, "")
|
||||||
const props = withDefaults(
|
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||||
defineProps<{
|
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||||
modelValue: HoppGQLRequest
|
|
||||||
response?: GQLResponseEvent[] | null
|
const idTicker = ref(0)
|
||||||
tabId: string
|
|
||||||
}>(),
|
const bulkMode = ref(false)
|
||||||
|
const bulkHeaders = ref("")
|
||||||
|
const bulkEditor = ref<any | null>(null)
|
||||||
|
const linewrapEnabled = ref(true)
|
||||||
|
|
||||||
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(
|
||||||
|
bulkEditor,
|
||||||
|
bulkHeaders,
|
||||||
|
reactive({
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "text/x-yaml",
|
||||||
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
|
lineWrapping: linewrapEnabled,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
environmentHighlights: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// The functional headers list (the headers actually in the system)
|
||||||
|
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
|
||||||
|
|
||||||
|
const auth = useStream(
|
||||||
|
gqlAuth$,
|
||||||
|
{ authType: "none", authActive: true },
|
||||||
|
setGQLAuth
|
||||||
|
)
|
||||||
|
|
||||||
|
// The UI representation of the headers list (has the empty end header)
|
||||||
|
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
|
||||||
{
|
{
|
||||||
response: null,
|
id: idTicker.value++,
|
||||||
}
|
key: "",
|
||||||
)
|
value: "",
|
||||||
const emit = defineEmits(["update:modelValue", "update:response"])
|
active: true,
|
||||||
|
|
||||||
const request = ref(props.modelValue)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => request.value,
|
|
||||||
(newVal) => {
|
|
||||||
emit("update:modelValue", newVal)
|
|
||||||
},
|
},
|
||||||
{ deep: true }
|
])
|
||||||
|
|
||||||
|
// Rule: Working Headers always have one empty header or the last element is always an empty header
|
||||||
|
watch(workingHeaders, (headersList) => {
|
||||||
|
if (
|
||||||
|
headersList.length > 0 &&
|
||||||
|
headersList[headersList.length - 1].key !== ""
|
||||||
|
) {
|
||||||
|
workingHeaders.value.push({
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync logic between headers and working headers
|
||||||
|
watch(
|
||||||
|
headers,
|
||||||
|
(newHeadersList) => {
|
||||||
|
// Sync should overwrite working headers
|
||||||
|
const filteredWorkingHeaders = pipe(
|
||||||
|
workingHeaders.value,
|
||||||
|
A.filterMap(
|
||||||
|
flow(
|
||||||
|
O.fromPredicate((e) => e.key !== ""),
|
||||||
|
O.map(objRemoveKey("id"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredBulkHeaders = pipe(
|
||||||
|
parseRawKeyValueEntriesE(bulkHeaders.value),
|
||||||
|
E.map(
|
||||||
|
flow(
|
||||||
|
RA.filter((e) => e.key !== ""),
|
||||||
|
RA.toArray
|
||||||
|
)
|
||||||
|
),
|
||||||
|
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
|
||||||
|
workingHeaders.value = pipe(
|
||||||
|
newHeadersList,
|
||||||
|
A.map((x) => ({ id: idTicker.value++, ...x }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
|
||||||
|
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const url = computedWithControl(
|
watch(workingHeaders, (newWorkingHeaders) => {
|
||||||
() => currentActiveTab.value,
|
const fixedHeaders = pipe(
|
||||||
() => currentActiveTab.value.document.request.url
|
newWorkingHeaders,
|
||||||
)
|
A.filterMap(
|
||||||
|
flow(
|
||||||
|
O.fromPredicate((e) => e.key !== ""),
|
||||||
|
O.map(objRemoveKey("id"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(headers.value, fixedHeaders)) {
|
||||||
|
headers.value = cloneDeep(fixedHeaders)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bulk Editor Syncing with Working Headers
|
||||||
|
watch(bulkHeaders, (newBulkHeaders) => {
|
||||||
|
const filteredBulkHeaders = pipe(
|
||||||
|
parseRawKeyValueEntriesE(newBulkHeaders),
|
||||||
|
E.map(
|
||||||
|
flow(
|
||||||
|
RA.filter((e) => e.key !== ""),
|
||||||
|
RA.toArray
|
||||||
|
)
|
||||||
|
),
|
||||||
|
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(headers.value, filteredBulkHeaders)) {
|
||||||
|
headers.value = filteredBulkHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(workingHeaders, (newHeadersList) => {
|
||||||
|
// If we are in bulk mode, don't apply direct changes
|
||||||
|
if (bulkMode.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
|
||||||
|
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
|
||||||
|
value: item.substring(item.indexOf(":") + 1).trimLeft(),
|
||||||
|
active: !item.trim().startsWith("#"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
|
||||||
|
|
||||||
|
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
|
||||||
|
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(`${t("error.something_went_wrong")}`)
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addHeader = () => {
|
||||||
|
workingHeaders.value.push({
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
|
||||||
|
workingHeaders.value = workingHeaders.value.map((h, i) =>
|
||||||
|
i === index ? header : h
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHeader = (index: number) => {
|
||||||
|
const headersBeforeDeletion = clone(workingHeaders.value)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
headersBeforeDeletion.length > 0 &&
|
||||||
|
index === headersBeforeDeletion.length - 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (deletionToast.value) {
|
||||||
|
deletionToast.value.goAway(0)
|
||||||
|
deletionToast.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
text: `${t("action.undo")}`,
|
||||||
|
onClick: (_, toastObject) => {
|
||||||
|
workingHeaders.value = headersBeforeDeletion
|
||||||
|
toastObject.goAway(0)
|
||||||
|
deletionToast.value = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
onComplete: () => {
|
||||||
|
deletionToast.value = null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
workingHeaders.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearContent = () => {
|
||||||
|
// set headers list to the initial state
|
||||||
|
workingHeaders.value = [
|
||||||
|
{
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
bulkHeaders.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
const activeGQLHeadersCount = computed(
|
const activeGQLHeadersCount = computed(
|
||||||
() =>
|
() =>
|
||||||
request.value.headers.filter(
|
headers.value.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
||||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
.length
|
||||||
).length
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const variableEditor = ref<any | null>(null)
|
||||||
|
const linewrapEnabledVariable = ref(true)
|
||||||
|
|
||||||
|
useCodemirror(
|
||||||
|
variableEditor,
|
||||||
|
variableString,
|
||||||
|
reactive({
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "application/ld+json",
|
||||||
|
placeholder: `${t("request.variables")}`,
|
||||||
|
lineWrapping: linewrapEnabledVariable,
|
||||||
|
},
|
||||||
|
linter: computed(() =>
|
||||||
|
variableString.value.length > 0 ? jsonLinter : null
|
||||||
|
),
|
||||||
|
completer: null,
|
||||||
|
environmentHighlights: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryEditor = ref<any | null>(null)
|
||||||
|
const schema = useReadonlyStream(props.conn.schema$, null, "noclone")
|
||||||
|
const linewrapEnabledQuery = ref(true)
|
||||||
|
|
||||||
|
useCodemirror(
|
||||||
|
queryEditor,
|
||||||
|
gqlQueryString,
|
||||||
|
reactive({
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "graphql",
|
||||||
|
placeholder: `${t("request.query")}`,
|
||||||
|
lineWrapping: linewrapEnabledQuery,
|
||||||
|
},
|
||||||
|
linter: createGQLQueryLinter(schema),
|
||||||
|
completer: queryCompleter(schema),
|
||||||
|
environmentHighlights: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
|
IconCopy,
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
|
IconCopy,
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
const prettifyQueryIcon = refAutoReset<
|
||||||
|
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||||
|
>(IconWand2, 1000)
|
||||||
|
const prettifyVariablesIcon = refAutoReset<
|
||||||
|
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||||
|
>(IconWand2, 1000)
|
||||||
|
|
||||||
const showSaveRequestModal = ref(false)
|
const showSaveRequestModal = ref(false)
|
||||||
const runQuery = async (
|
|
||||||
definition: gql.OperationDefinitionNode | null = null
|
const copyQuery = () => {
|
||||||
) => {
|
copyToClipboard(gqlQueryString.value)
|
||||||
|
copyQueryIcon.value = IconCheck
|
||||||
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = useStream(gqlResponse$, "", setGQLResponse)
|
||||||
|
|
||||||
|
const runQuery = async () => {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
startPageProgress()
|
startPageProgress()
|
||||||
|
response.value = "loading"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runURL = clone(url.value)
|
const runURL = clone(url.value)
|
||||||
const runHeaders = clone(request.value.headers)
|
const runHeaders = clone(headers.value)
|
||||||
const runQuery = clone(request.value.query)
|
const runQuery = clone(gqlQueryString.value)
|
||||||
const runVariables = clone(request.value.variables)
|
const runVariables = clone(variableString.value)
|
||||||
const runAuth = clone(request.value.auth)
|
const runAuth = clone(auth.value)
|
||||||
|
|
||||||
await runGQLOperation({
|
const responseText = await props.conn.runQuery(
|
||||||
name: request.value.name,
|
runURL,
|
||||||
url: runURL,
|
runHeaders,
|
||||||
headers: runHeaders,
|
runQuery,
|
||||||
query: runQuery,
|
runVariables,
|
||||||
variables: runVariables,
|
runAuth
|
||||||
auth: runAuth,
|
)
|
||||||
operationName: definition?.name?.value,
|
|
||||||
operationType: definition?.operation ?? "query",
|
|
||||||
})
|
|
||||||
const duration = Date.now() - startTime
|
const duration = Date.now() - startTime
|
||||||
|
|
||||||
completePageProgress()
|
completePageProgress()
|
||||||
|
|
||||||
|
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
||||||
|
|
||||||
|
addGraphqlHistoryEntry(
|
||||||
|
makeGQLHistoryEntry({
|
||||||
|
request: makeGQLRequest({
|
||||||
|
name: "",
|
||||||
|
url: runURL,
|
||||||
|
query: runQuery,
|
||||||
|
headers: runHeaders,
|
||||||
|
variables: runVariables,
|
||||||
|
auth: runAuth,
|
||||||
|
}),
|
||||||
|
response: response.value,
|
||||||
|
star: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
toast.success(`${t("state.finished_in", { duration })}`)
|
toast.success(`${t("state.finished_in", { duration })}`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log(e)
|
response.value = `${e}`
|
||||||
// response.value = [`${e}`]
|
|
||||||
completePageProgress()
|
completePageProgress()
|
||||||
|
|
||||||
toast.error(
|
toast.error(
|
||||||
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
|
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_REQUEST_RUN",
|
type: "HOPP_REQUEST_RUN",
|
||||||
platform: "graphql-query",
|
platform: "graphql-query",
|
||||||
@@ -153,57 +751,55 @@ const runQuery = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => gqlMessageEvent.value,
|
|
||||||
(event) => {
|
|
||||||
if (event === "reset") {
|
|
||||||
emit("update:response", [])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (event?.operationType !== "subscription") {
|
|
||||||
// response.value = [event]
|
|
||||||
emit("update:response", [event])
|
|
||||||
} else {
|
|
||||||
emit("update:response", [...(props.response ?? []), event])
|
|
||||||
|
|
||||||
// TODO: subscription indicator??
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const hideRequestModal = () => {
|
const hideRequestModal = () => {
|
||||||
showSaveRequestModal.value = false
|
showSaveRequestModal.value = false
|
||||||
}
|
}
|
||||||
const saveRequest = () => {
|
|
||||||
if (
|
|
||||||
currentActiveTab.value.document.saveContext &&
|
|
||||||
currentActiveTab.value.document.saveContext.originLocation ===
|
|
||||||
"user-collection"
|
|
||||||
) {
|
|
||||||
editGraphqlRequest(
|
|
||||||
currentActiveTab.value.document.saveContext.folderPath,
|
|
||||||
currentActiveTab.value.document.saveContext.requestIndex,
|
|
||||||
currentActiveTab.value.document.request
|
|
||||||
)
|
|
||||||
|
|
||||||
currentActiveTab.value.document.isDirty = false
|
const prettifyQuery = () => {
|
||||||
} else {
|
try {
|
||||||
showSaveRequestModal.value = true
|
gqlQueryString.value = gql.print(
|
||||||
|
gql.parse(gqlQueryString.value, {
|
||||||
|
allowLegacyFragmentVariables: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
prettifyQueryIcon.value = IconCheck
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(`${t("error.gql_prettify_invalid_query")}`)
|
||||||
|
prettifyQueryIcon.value = IconInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const clearGQLQuery = () => {
|
|
||||||
request.value.query = ""
|
const saveRequest = () => {
|
||||||
|
showSaveRequestModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyVariables = () => {
|
||||||
|
copyToClipboard(variableString.value)
|
||||||
|
copyVariablesIcon.value = IconCheck
|
||||||
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prettifyVariableString = () => {
|
||||||
|
try {
|
||||||
|
const jsonObj = JSON.parse(variableString.value)
|
||||||
|
variableString.value = JSON.stringify(jsonObj, null, 2)
|
||||||
|
prettifyVariablesIcon.value = IconCheck
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
prettifyVariablesIcon.value = IconInfo
|
||||||
|
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearGQLQuery = () => {
|
||||||
|
gqlQueryString.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearGQLVariables = () => {
|
||||||
|
variableString.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
defineActionHandler("request.send-cancel", runQuery)
|
defineActionHandler("request.send-cancel", runQuery)
|
||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
defineActionHandler("request.save-as", () => {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
})
|
|
||||||
defineActionHandler("request.reset", clearGQLQuery)
|
defineActionHandler("request.reset", clearGQLQuery)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AppPaneLayout layout-id="gql-primary">
|
|
||||||
<template #primary>
|
|
||||||
<GraphqlRequestOptions
|
|
||||||
v-model="tab.document.request"
|
|
||||||
v-model:response="tab.response"
|
|
||||||
:tab-id="tab.id"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #secondary>
|
|
||||||
<GraphqlResponse :response="tab.response" />
|
|
||||||
</template>
|
|
||||||
</AppPaneLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from "@vueuse/core"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import { watch } from "vue"
|
|
||||||
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
|
|
||||||
import { HoppGQLTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
// TODO: Move Response and Request execution code to over here
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: HoppGQLTab }>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", val: HoppGQLTab): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const tab = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
// TODO: Come up with a better dirty check
|
|
||||||
let oldRequest = cloneDeep(tab.value.document.request)
|
|
||||||
watch(
|
|
||||||
() => tab.value.document.request,
|
|
||||||
(updatedValue) => {
|
|
||||||
// TODO: Check equality of request
|
|
||||||
if (
|
|
||||||
!tab.value.document.isDirty &&
|
|
||||||
!isEqualHoppGQLRequest(oldRequest, updatedValue)
|
|
||||||
) {
|
|
||||||
tab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
oldRequest = cloneDeep(updatedValue)
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
||||||
<div v-if="response?.length === 1" class="flex flex-col flex-1">
|
<HoppSmartPlaceholder
|
||||||
|
v-if="responseString === 'loading'"
|
||||||
|
:text="t('state.loading')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<HoppSmartSpinner class="my-4" />
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
<div v-else-if="responseString" class="flex flex-col flex-1">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
@@ -29,18 +37,12 @@
|
|||||||
'action.copy'
|
'action.copy'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
||||||
:icon="copyResponseIcon"
|
:icon="copyResponseIcon"
|
||||||
@click="copyResponse(response[0].data)"
|
@click="copyResponse"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="schemaEditor" class="flex flex-col flex-1"></div>
|
<div ref="schemaEditor" class="flex flex-col flex-1"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else-if="response && response?.length > 1"
|
|
||||||
class="flex flex-col flex-1"
|
|
||||||
>
|
|
||||||
<GraphqlSubscriptionLog :log="response" />
|
|
||||||
</div>
|
|
||||||
<AppShortcutsPrompt v-else class="p-4" />
|
<AppShortcutsPrompt v-else class="p-4" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,34 +52,22 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
|||||||
import IconDownload from "~icons/lucide/download"
|
import IconDownload from "~icons/lucide/download"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import { computed, reactive, ref } from "vue"
|
import { reactive, ref } from "vue"
|
||||||
import { refAutoReset } from "@vueuse/core"
|
import { refAutoReset } from "@vueuse/core"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
|
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const props = withDefaults(
|
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||||
defineProps<{
|
|
||||||
response: GQLResponseEvent[] | null
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
response: null,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const responseString = computed(() => {
|
|
||||||
if (props.response?.length === 1) {
|
|
||||||
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
@@ -105,14 +95,14 @@ const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
1000
|
1000
|
||||||
)
|
)
|
||||||
|
|
||||||
const copyResponse = (str: string) => {
|
const copyResponse = () => {
|
||||||
copyToClipboard(str)
|
copyToClipboard(responseString.value!)
|
||||||
copyResponseIcon.value = IconCheck
|
copyResponseIcon.value = IconCheck
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadResponse = (str: string) => {
|
const downloadResponse = () => {
|
||||||
const dataToWrite = str
|
const dataToWrite = responseString.value
|
||||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||||
const a = document.createElement("a")
|
const a = document.createElement("a")
|
||||||
const url = URL.createObjectURL(file)
|
const url = URL.createObjectURL(file)
|
||||||
@@ -128,8 +118,6 @@ const downloadResponse = (str: string) => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("response.file.download", () =>
|
defineActionHandler("response.file.download", () => downloadResponse())
|
||||||
downloadResponse(responseString.value)
|
defineActionHandler("response.copy", () => copyResponse())
|
||||||
)
|
|
||||||
defineActionHandler("response.copy", () => copyResponse(responseString.value))
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,20 @@
|
|||||||
vertical
|
vertical
|
||||||
render-inactive-tabs
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
|
<HoppSmartTab
|
||||||
|
:id="'history'"
|
||||||
|
:icon="IconClock"
|
||||||
|
:label="`${t('tab.history')}`"
|
||||||
|
>
|
||||||
|
<History :page="'graphql'" @use-history="handleUseHistory" />
|
||||||
|
</HoppSmartTab>
|
||||||
|
<HoppSmartTab
|
||||||
|
:id="'collections'"
|
||||||
|
:icon="IconFolder"
|
||||||
|
:label="`${t('tab.collections')}`"
|
||||||
|
>
|
||||||
|
<CollectionsGraphql />
|
||||||
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'docs'"
|
:id="'docs'"
|
||||||
:icon="IconBookOpen"
|
:icon="IconBookOpen"
|
||||||
@@ -159,21 +173,6 @@
|
|||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
|
||||||
<HoppSmartTab
|
|
||||||
:id="'collections'"
|
|
||||||
:icon="IconFolder"
|
|
||||||
:label="`${t('tab.collections')}`"
|
|
||||||
>
|
|
||||||
<CollectionsGraphql />
|
|
||||||
</HoppSmartTab>
|
|
||||||
<HoppSmartTab
|
|
||||||
:id="'history'"
|
|
||||||
:icon="IconClock"
|
|
||||||
:label="`${t('tab.history')}`"
|
|
||||||
>
|
|
||||||
<History :page="'graphql'" />
|
|
||||||
</HoppSmartTab>
|
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -189,24 +188,29 @@ import IconCopy from "~icons/lucide/copy"
|
|||||||
import IconBox from "~icons/lucide/box"
|
import IconBox from "~icons/lucide/box"
|
||||||
import { computed, nextTick, reactive, ref } from "vue"
|
import { computed, nextTick, reactive, ref } from "vue"
|
||||||
import { GraphQLField, GraphQLType } from "graphql"
|
import { GraphQLField, GraphQLType } from "graphql"
|
||||||
|
import { map } from "rxjs/operators"
|
||||||
|
import { GQLHeader } from "@hoppscotch/data"
|
||||||
import { refAutoReset } from "@vueuse/core"
|
import { refAutoReset } from "@vueuse/core"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
|
import { GQLConnection } from "@helpers/GQLConnection"
|
||||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||||
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import {
|
import {
|
||||||
graphqlTypes,
|
setGQLAuth,
|
||||||
mutationFields,
|
setGQLHeaders,
|
||||||
queryFields,
|
setGQLQuery,
|
||||||
schemaString,
|
setGQLResponse,
|
||||||
subscriptionFields,
|
setGQLURL,
|
||||||
} from "~/helpers/graphql/connection"
|
setGQLVariables,
|
||||||
|
} from "~/newstore/GQLSession"
|
||||||
|
|
||||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||||
|
|
||||||
const selectedNavigationTab = ref<NavigationTabs>("docs")
|
const selectedNavigationTab = ref<NavigationTabs>("history")
|
||||||
const selectedGqlTab = ref<GqlTabs>("queries")
|
const selectedGqlTab = ref<GqlTabs>("queries")
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -266,8 +270,40 @@ function resolveRootType(type: GraphQLType) {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GQLHistoryEntry = {
|
||||||
|
url: string
|
||||||
|
headers: GQLHeader[]
|
||||||
|
query: string
|
||||||
|
response: string
|
||||||
|
variables: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
conn: GQLConnection
|
||||||
|
}>()
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
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 = refAutoReset<typeof IconDownload | typeof IconCheck>(
|
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
|
||||||
IconDownload,
|
IconDownload,
|
||||||
1000
|
1000
|
||||||
@@ -354,6 +390,11 @@ const handleJumpToType = async (type: GraphQLType) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const schemaString = useReadonlyStream(
|
||||||
|
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
|
|
||||||
@@ -395,4 +436,23 @@ const copySchema = () => {
|
|||||||
copyToClipboard(schemaString.value)
|
copyToClipboard(schemaString.value)
|
||||||
copySchemaIcon.value = IconCheck
|
copySchemaIcon.value = IconCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
setGQLAuth({
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
})
|
||||||
|
props.conn.reset()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="container" class="flex flex-col flex-1">
|
|
||||||
<div
|
|
||||||
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
|
|
||||||
>
|
|
||||||
<label for="log" class="py-2 font-semibold text-secondaryLight">
|
|
||||||
{{ "Subscription Log" }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.delete')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
@click="emit('delete')"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="bottompage"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.scroll_to_top')"
|
|
||||||
:icon="IconArrowUp"
|
|
||||||
@click="scrollTo('top')"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="bottompage"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.scroll_to_bottom')"
|
|
||||||
:icon="IconArrowDown"
|
|
||||||
@click="scrollTo('bottom')"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="bottompage"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.autoscroll')"
|
|
||||||
:icon="IconChevronsDown"
|
|
||||||
:class="toggleAutoscrollColor"
|
|
||||||
@click="toggleAutoscroll()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="log.length !== 0"
|
|
||||||
ref="logs"
|
|
||||||
class="overflow-y-auto border-b border-dividerLight"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
|
|
||||||
>
|
|
||||||
<RealtimeLogEntry
|
|
||||||
v-for="(entry, index) in log"
|
|
||||||
:key="`entry-${index}`"
|
|
||||||
:is-open="log.length - 1 === index"
|
|
||||||
:entry="{ ts: entry.time, source: 'info', payload: entry.data }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
|
||||||
import IconTrash from "~icons/lucide/trash"
|
|
||||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
|
||||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
|
||||||
import IconChevronsDown from "~icons/lucide/chevron-down"
|
|
||||||
import { useThrottleFn, useScroll } from "@vueuse/core"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
log: { type: Array as PropType<GQLResponseEvent[]>, default: () => [] },
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "delete"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const container = ref<HTMLElement | null>(null)
|
|
||||||
const logs = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const autoScrollEnabled = ref(true)
|
|
||||||
|
|
||||||
const logListScroll = useScroll(logs as Ref<HTMLElement>)
|
|
||||||
|
|
||||||
// Disable autoscroll when scrolling to top
|
|
||||||
watch(logListScroll.isScrolling, (isScrolling) => {
|
|
||||||
if (isScrolling && logListScroll.directions.top)
|
|
||||||
autoScrollEnabled.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollTo = (position: "top" | "bottom") => {
|
|
||||||
if (position === "top") {
|
|
||||||
logs.value?.scroll({
|
|
||||||
behavior: "smooth",
|
|
||||||
top: 0,
|
|
||||||
})
|
|
||||||
} else if (position === "bottom") {
|
|
||||||
logs.value?.scroll({
|
|
||||||
behavior: "smooth",
|
|
||||||
top: logs.value?.scrollHeight,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.log,
|
|
||||||
useThrottleFn(() => {
|
|
||||||
if (autoScrollEnabled.value) scrollTo("bottom")
|
|
||||||
}, 200),
|
|
||||||
{ flush: "post" }
|
|
||||||
)
|
|
||||||
|
|
||||||
const toggleAutoscroll = () => {
|
|
||||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleAutoscrollColor = computed(() =>
|
|
||||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
|
||||||
:title="tab.document.request.name"
|
|
||||||
class="truncate px-2 flex items-center"
|
|
||||||
@dblclick="emit('open-rename-modal')"
|
|
||||||
@contextmenu.prevent="options?.tippy?.show()"
|
|
||||||
@click.middle="emit('close-tab')"
|
|
||||||
>
|
|
||||||
<tippy
|
|
||||||
ref="options"
|
|
||||||
trigger="manual"
|
|
||||||
interactive
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions!.focus()"
|
|
||||||
>
|
|
||||||
<span class="leading-8 px-2 truncate">
|
|
||||||
{{ tab.document.request.name }}
|
|
||||||
</span>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.r="renameAction?.$el.click()"
|
|
||||||
@keyup.d="duplicateAction?.$el.click()"
|
|
||||||
@keyup.w="closeAction?.$el.click()"
|
|
||||||
@keyup.x="closeOthersAction?.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="renameAction"
|
|
||||||
:icon="IconFileEdit"
|
|
||||||
:label="t('request.rename')"
|
|
||||||
:shortcut="['R']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('open-rename-modal')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="duplicateAction"
|
|
||||||
:icon="IconCopy"
|
|
||||||
:label="t('tab.duplicate')"
|
|
||||||
:shortcut="['D']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('duplicate-tab')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-if="isRemovable"
|
|
||||||
ref="closeAction"
|
|
||||||
:icon="IconXCircle"
|
|
||||||
:label="t('tab.close')"
|
|
||||||
:shortcut="['W']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('close-tab')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-if="isRemovable"
|
|
||||||
ref="closeOthersAction"
|
|
||||||
:icon="IconXSquare"
|
|
||||||
:label="t('tab.close_others')"
|
|
||||||
:shortcut="['X']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('close-other-tabs')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from "vue"
|
|
||||||
import { TippyComponent } from "vue-tippy"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
import IconXCircle from "~icons/lucide/x-circle"
|
|
||||||
import IconXSquare from "~icons/lucide/x-square"
|
|
||||||
import IconFileEdit from "~icons/lucide/file-edit"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import { HoppGQLTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
tab: HoppGQLTab
|
|
||||||
isRemovable: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "open-rename-modal"): void
|
|
||||||
(event: "close-tab"): void
|
|
||||||
(event: "close-other-tabs"): void
|
|
||||||
(event: "duplicate-tab"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
|
||||||
const options = ref<TippyComponent | null>(null)
|
|
||||||
|
|
||||||
const renameAction = ref<HTMLButtonElement | null>(null)
|
|
||||||
const closeAction = ref<HTMLButtonElement | null>(null)
|
|
||||||
const closeOthersAction = ref<HTMLButtonElement | null>(null)
|
|
||||||
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
|
||||||
</script>
|
|
||||||
@@ -55,48 +55,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
|
// TODO: TypeScript + Setup Script this at some point :)
|
||||||
|
|
||||||
|
import { defineComponent } from "vue"
|
||||||
import {
|
import {
|
||||||
GraphQLEnumType,
|
GraphQLEnumType,
|
||||||
GraphQLInputObjectType,
|
GraphQLInputObjectType,
|
||||||
GraphQLInterfaceType,
|
GraphQLInterfaceType,
|
||||||
} from "graphql"
|
} from "graphql"
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
const props = defineProps({
|
export default defineComponent({
|
||||||
gqlType: {
|
props: {
|
||||||
type: Object,
|
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
|
||||||
required: true,
|
gqlType: {},
|
||||||
|
gqlTypes: { type: Array, default: () => [] },
|
||||||
|
jumpTypeCallback: { type: Function, default: () => ({}) },
|
||||||
|
isHighlighted: { type: Boolean, default: false },
|
||||||
|
highlightedFields: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isInput() {
|
||||||
|
return this.gqlType instanceof GraphQLInputObjectType
|
||||||
|
},
|
||||||
|
isInterface() {
|
||||||
|
return this.gqlType instanceof GraphQLInterfaceType
|
||||||
|
},
|
||||||
|
isEnum() {
|
||||||
|
return this.gqlType instanceof GraphQLEnumType
|
||||||
|
},
|
||||||
|
interfaces() {
|
||||||
|
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
|
||||||
|
},
|
||||||
|
children() {
|
||||||
|
return this.gqlTypes.filter(
|
||||||
|
(type) =>
|
||||||
|
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isFieldHighlighted({ field }) {
|
||||||
|
return !!this.highlightedFields.find(({ name }) => name === field.name)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gqlTypes: { type: Array, default: () => [] },
|
|
||||||
jumpTypeCallback: { type: Function, default: () => ({}) },
|
|
||||||
isHighlighted: { type: Boolean, default: false },
|
|
||||||
highlightedFields: { type: Array, default: () => [] },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isInput = computed(() => {
|
|
||||||
return props.gqlType instanceof GraphQLInputObjectType
|
|
||||||
})
|
|
||||||
|
|
||||||
const isInterface = computed(() => {
|
|
||||||
return props.gqlType instanceof GraphQLInterfaceType
|
|
||||||
})
|
|
||||||
const isEnum = computed(() => {
|
|
||||||
return props.gqlType instanceof GraphQLEnumType
|
|
||||||
})
|
|
||||||
const interfaces = computed(() => {
|
|
||||||
return (props.gqlType.getInterfaces && props.gqlType.getInterfaces()) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
const children = computed(() => {
|
|
||||||
return props.gqlTypes.filter(
|
|
||||||
(type) => type.getInterfaces && type.getInterfaces().includes(props.gqlType)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFieldHighlighted = ({ field }) => {
|
|
||||||
return !!props.highlightedFields.find(({ name }) => name === field.name)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
|
||||||
>
|
|
||||||
<label class="font-semibold text-secondaryLight">
|
|
||||||
{{ t("request.variables") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="subscriptionState === 'SUBSCRIBED'"
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
allowHTML: true,
|
|
||||||
}"
|
|
||||||
:title="`${t('request.stop')}`"
|
|
||||||
:label="`${t('request.stop')}`"
|
|
||||||
:icon="IconStop"
|
|
||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
|
||||||
@click="unsubscribe()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
allowHTML: true,
|
|
||||||
}"
|
|
||||||
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
|
||||||
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
|
|
||||||
:icon="IconPlay"
|
|
||||||
:disabled="!selectedOperation"
|
|
||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
|
||||||
@click="runQuery(selectedOperation)"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
|
||||||
blank
|
|
||||||
:title="t('app.wiki')"
|
|
||||||
:icon="IconHelpCircle"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
@click="clearGQLVariables()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.linewrap')"
|
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
|
||||||
:icon="IconWrapText"
|
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.prettify')"
|
|
||||||
:icon="prettifyVariablesIcon"
|
|
||||||
@click="prettifyVariableString"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.copy')"
|
|
||||||
:icon="copyVariablesIcon"
|
|
||||||
@click="copyVariables"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref="variableEditor" class="flex flex-col flex-1"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconPlay from "~icons/lucide/play"
|
|
||||||
import IconStop from "~icons/lucide/stop-circle"
|
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
import IconInfo from "~icons/lucide/info"
|
|
||||||
import IconWand from "~icons/lucide/wand"
|
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
|
||||||
import { computed, reactive, ref } from "vue"
|
|
||||||
import jsonLinter from "~/helpers/editor/linting/json"
|
|
||||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
|
||||||
import * as gql from "graphql"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import {
|
|
||||||
socketDisconnect,
|
|
||||||
subscriptionState,
|
|
||||||
} from "~/helpers/graphql/connection"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "save-request"): void
|
|
||||||
(e: "update:modelValue", val: string): void
|
|
||||||
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// Watch operations on graphql query string
|
|
||||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
|
||||||
|
|
||||||
const variableString = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
const variableEditor = ref<any | null>(null)
|
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
|
||||||
|
|
||||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|
||||||
IconCopy,
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
const prettifyVariablesIcon = refAutoReset<
|
|
||||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
|
||||||
>(IconWand, 1000)
|
|
||||||
|
|
||||||
useCodemirror(
|
|
||||||
variableEditor,
|
|
||||||
variableString,
|
|
||||||
reactive({
|
|
||||||
extendedEditorConfig: {
|
|
||||||
mode: "application/ld+json",
|
|
||||||
placeholder: `${t("request.variables")}`,
|
|
||||||
lineWrapping: linewrapEnabled,
|
|
||||||
},
|
|
||||||
linter: computed(() =>
|
|
||||||
variableString.value.length > 0 ? jsonLinter : null
|
|
||||||
),
|
|
||||||
completer: null,
|
|
||||||
environmentHighlights: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const copyVariables = () => {
|
|
||||||
copyToClipboard(variableString.value)
|
|
||||||
copyVariablesIcon.value = IconCheck
|
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const prettifyVariableString = () => {
|
|
||||||
try {
|
|
||||||
const jsonObj = JSON.parse(variableString.value)
|
|
||||||
variableString.value = JSON.stringify(jsonObj, null, 2)
|
|
||||||
prettifyVariablesIcon.value = IconCheck
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
prettifyVariablesIcon.value = IconInfo
|
|
||||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearGQLVariables = () => {
|
|
||||||
variableString.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
|
|
||||||
emit("run-query", definition)
|
|
||||||
}
|
|
||||||
const unsubscribe = () => {
|
|
||||||
socketDisconnect()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -56,6 +56,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
|
import { makeGQLRequest } from "@hoppscotch/data"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { setGQLSession } from "~/newstore/GQLSession"
|
||||||
import { GQLHistoryEntry } from "~/newstore/history"
|
import { GQLHistoryEntry } from "~/newstore/history"
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
import { shortDateTime } from "~/helpers/utils/date"
|
||||||
|
|
||||||
@@ -66,8 +69,6 @@ import IconMinimize2 from "~icons/lucide/minimize-2"
|
|||||||
import IconMaximize2 from "~icons/lucide/maximize-2"
|
import IconMaximize2 from "~icons/lucide/maximize-2"
|
||||||
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { makeGQLRequest } from "@hoppscotch/data"
|
|
||||||
import { createNewTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -93,16 +94,19 @@ const query = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const useEntry = () => {
|
const useEntry = () => {
|
||||||
createNewTab({
|
setGQLSession({
|
||||||
request: makeGQLRequest({
|
request: cloneDeep(
|
||||||
name: props.entry.request.name,
|
makeGQLRequest({
|
||||||
url: props.entry.request.url,
|
name: props.entry.request.name,
|
||||||
headers: props.entry.request.headers,
|
url: props.entry.request.url,
|
||||||
query: props.entry.request.query,
|
headers: props.entry.request.headers,
|
||||||
variables: props.entry.request.variables,
|
query: props.entry.request.query,
|
||||||
auth: props.entry.request.auth,
|
variables: props.entry.request.variables,
|
||||||
}),
|
auth: props.entry.request.auth,
|
||||||
isDirty: false,
|
})
|
||||||
|
),
|
||||||
|
schema: "",
|
||||||
|
response: props.entry.response,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
blank
|
blank
|
||||||
:icon="IconExternalLink"
|
:icon="IconExternalLink"
|
||||||
reverse
|
reverse
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
blank
|
blank
|
||||||
:icon="IconExternalLink"
|
:icon="IconExternalLink"
|
||||||
reverse
|
reverse
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
|
class="mb-4"
|
||||||
@click="addBodyParam"
|
@click="addBodyParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -213,6 +213,7 @@
|
|||||||
filled
|
filled
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
|
class="mb-4"
|
||||||
@click="addHeader"
|
@click="addHeader"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -33,11 +33,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import {
|
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
||||||
HoppGQLAuthOAuth2,
|
|
||||||
HoppRESTAuthOAuth2,
|
|
||||||
parseTemplateString,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
@@ -48,7 +44,7 @@ const t = useI18n()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
modelValue: HoppRESTAuthOAuth2
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -161,6 +161,7 @@
|
|||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
filled
|
filled
|
||||||
|
class="mb-4"
|
||||||
@click="addParam"
|
@click="addParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -56,7 +56,13 @@
|
|||||||
:inspection-results="tabResults"
|
:inspection-results="tabResults"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@enter="newSendRequest"
|
@enter="newSendRequest"
|
||||||
/>
|
>
|
||||||
|
<template #empty>
|
||||||
|
<span>
|
||||||
|
{{ t("empty.history_suggestions") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</SmartEnvInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 sm:mt-0">
|
<div class="flex mt-2 sm:mt-0">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:title="tab.document.request.name"
|
:title="tab.document.request.name"
|
||||||
class="truncate px-2 flex items-center"
|
class="truncate px-2 flex items-center"
|
||||||
@dblclick="emit('open-rename-modal')"
|
@dblclick="emit('open-rename-modal')"
|
||||||
@contextmenu.prevent="options?.tippy?.show()"
|
@contextmenu.prevent="options?.tippy.show()"
|
||||||
@click.middle="emit('close-tab')"
|
@click.middle="emit('close-tab')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
filled
|
filled
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
|
class="mb-4"
|
||||||
@click="addUrlEncodedParam"
|
@click="addUrlEncodedParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
import { ref, PropType, computed, watch } from "vue"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||||
@@ -73,7 +73,7 @@ export type LogEntryData = {
|
|||||||
ts: number | undefined
|
ts: number | undefined
|
||||||
source: "info" | "client" | "server" | "disconnected"
|
source: "info" | "client" | "server" | "disconnected"
|
||||||
payload: string
|
payload: string
|
||||||
event?: "connecting" | "connected" | "disconnected" | "error"
|
event: "connecting" | "connected" | "disconnected" | "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -94,7 +94,7 @@ const logs = ref<HTMLElement>()
|
|||||||
|
|
||||||
const autoScrollEnabled = ref(true)
|
const autoScrollEnabled = ref(true)
|
||||||
|
|
||||||
const logListScroll = useScroll(logs as Ref<HTMLElement>)
|
const logListScroll = useScroll(logs)
|
||||||
|
|
||||||
// Disable autoscroll when scrolling to top
|
// Disable autoscroll when scrolling to top
|
||||||
watch(logListScroll.isScrolling, (isScrolling) => {
|
watch(logListScroll.isScrolling, (isScrolling) => {
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
|||||||
import * as LJSON from "lossless-json"
|
import * as LJSON from "lossless-json"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import { ref, computed, reactive, watch, markRaw, PropType } from "vue"
|
import { ref, computed, reactive, watch, markRaw } from "vue"
|
||||||
import { refAutoReset, useTimeAgo } from "@vueuse/core"
|
import { refAutoReset, useTimeAgo } from "@vueuse/core"
|
||||||
import { LogEntryData } from "./Log.vue"
|
import { LogEntryData } from "./Log.vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
@@ -227,16 +227,7 @@ import { shortDateTime } from "~/helpers/utils/date"
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps<{ entry: LogEntryData }>()
|
||||||
entry: {
|
|
||||||
type: Object as PropType<LogEntryData>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isOpen: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
@@ -313,7 +304,7 @@ const outlinePath = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Code for UI Changes
|
// Code for UI Changes
|
||||||
const minimized = ref(props.isOpen ? false : true)
|
const minimized = ref(true)
|
||||||
watch(minimized, () => {
|
watch(minimized, () => {
|
||||||
selectedTab.value = isJSON(props.entry.payload) ? "json" : "raw"
|
selectedTab.value = isJSON(props.entry.payload) ? "json" : "raw"
|
||||||
})
|
})
|
||||||
@@ -351,9 +342,7 @@ const ENTRY_COLORS = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Assigns color based on entry event
|
// Assigns color based on entry event
|
||||||
const entryColor = computed(
|
const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
|
||||||
() => props.entry.event && ENTRY_COLORS[props.entry.event]
|
|
||||||
)
|
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
info: {
|
info: {
|
||||||
|
|||||||
@@ -21,13 +21,17 @@
|
|||||||
<div class="flex items-center py-4 space-x-2">
|
<div class="flex items-center py-4 space-x-2">
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="PROXY_URL"
|
v-model="PROXY_URL"
|
||||||
:autofocus="false"
|
|
||||||
styles="flex-1"
|
styles="flex-1"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
:label="t('settings.proxy_url')"
|
|
||||||
input-styles="input floating-input"
|
input-styles="input floating-input"
|
||||||
:disabled="!proxyEnabled"
|
:disabled="!proxyEnabled"
|
||||||
/>
|
>
|
||||||
|
<template #label>
|
||||||
|
<label for="url">
|
||||||
|
{{ t("settings.proxy_url") }}
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('settings.reset_default')"
|
:title="t('settings.reset_default')"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="autoCompleteWrapper" class="autocomplete-wrapper">
|
<div class="autocomplete-wrapper">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
|
class="absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
|
||||||
>
|
>
|
||||||
@@ -18,9 +18,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
v-if="
|
v-if="showSuggestionPopover && autoCompleteSource"
|
||||||
showSuggestionPopover && autoCompleteSource && suggestions.length > 0
|
|
||||||
"
|
|
||||||
ref="suggestionsMenu"
|
ref="suggestionsMenu"
|
||||||
class="suggestions"
|
class="suggestions"
|
||||||
>
|
>
|
||||||
@@ -41,12 +39,20 @@
|
|||||||
<span class="ml-2 truncate">to select</span>
|
<span class="ml-2 truncate">to select</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
||||||
|
<div v-if="slots.empty" class="truncate py-0.5">
|
||||||
|
<slot name="empty"></slot>
|
||||||
|
</div>
|
||||||
|
<span v-else class="truncate py-0.5">
|
||||||
|
{{ t("empty.suggestions") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, nextTick, computed, Ref } from "vue"
|
import { ref, onMounted, watch, nextTick, computed, Ref, useSlots } from "vue"
|
||||||
import {
|
import {
|
||||||
EditorView,
|
EditorView,
|
||||||
placeholder as placeholderExt,
|
placeholder as placeholderExt,
|
||||||
@@ -63,6 +69,7 @@ import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironme
|
|||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
import { InspectorResult } from "~/services/inspection"
|
import { InspectorResult } from "~/services/inspection"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
@@ -104,6 +111,10 @@ const emit = defineEmits<{
|
|||||||
(e: "click", ev: any): void
|
(e: "click", ev: any): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
const cachedValue = ref(props.modelValue)
|
const cachedValue = ref(props.modelValue)
|
||||||
|
|
||||||
const view = ref<EditorView>()
|
const view = ref<EditorView>()
|
||||||
@@ -114,9 +125,8 @@ const currentSuggestionIndex = ref(-1)
|
|||||||
const showSuggestionPopover = ref(false)
|
const showSuggestionPopover = ref(false)
|
||||||
|
|
||||||
const suggestionsMenu = ref<any | null>(null)
|
const suggestionsMenu = ref<any | null>(null)
|
||||||
const autoCompleteWrapper = ref<any | null>(null)
|
|
||||||
|
|
||||||
onClickOutside(autoCompleteWrapper, () => {
|
onClickOutside(suggestionsMenu, () => {
|
||||||
showSuggestionPopover.value = false
|
showSuggestionPopover.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -470,7 +480,7 @@ watch(editor, () => {
|
|||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
@apply flex-shrink-0;
|
@apply flex-shrink-0;
|
||||||
@apply whitespace-nowrap py-4;
|
@apply whitespace-nowrap;
|
||||||
|
|
||||||
.suggestions {
|
.suggestions {
|
||||||
@apply absolute;
|
@apply absolute;
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ type CodeMirrorOptions = {
|
|||||||
|
|
||||||
// NOTE: This property is not reactive
|
// NOTE: This property is not reactive
|
||||||
environmentHighlights: boolean
|
environmentHighlights: boolean
|
||||||
|
|
||||||
additionalExts?: Extension[]
|
|
||||||
|
|
||||||
// callback on editor update
|
|
||||||
onUpdate?: (view: ViewUpdate) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoppCompleterExt = (completer: Completer): Extension => {
|
const hoppCompleterExt = (completer: Completer): Extension => {
|
||||||
@@ -194,7 +189,6 @@ export function useCodemirror(
|
|||||||
): { cursor: Ref<{ line: number; ch: number }> } {
|
): { cursor: Ref<{ line: number; ch: number }> } {
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
const additionalExts = new Compartment()
|
|
||||||
const language = new Compartment()
|
const language = new Compartment()
|
||||||
const lineWrapping = new Compartment()
|
const lineWrapping = new Compartment()
|
||||||
const placeholderConfig = new Compartment()
|
const placeholderConfig = new Compartment()
|
||||||
@@ -260,24 +254,12 @@ export function useCodemirror(
|
|||||||
|
|
||||||
el.addEventListener("mouseup", debounceFn)
|
el.addEventListener("mouseup", debounceFn)
|
||||||
el.addEventListener("keyup", debounceFn)
|
el.addEventListener("keyup", debounceFn)
|
||||||
|
const cursorPos = update.state.selection.main.head
|
||||||
|
const line = update.state.doc.lineAt(cursorPos)
|
||||||
|
|
||||||
if (options.onUpdate) {
|
cachedCursor.value = {
|
||||||
options.onUpdate(update)
|
line: line.number - 1,
|
||||||
}
|
ch: cursorPos - line.from,
|
||||||
|
|
||||||
if (update.selectionSet) {
|
|
||||||
const cursorPos = update.state.selection.main.head
|
|
||||||
const line = update.state.doc.lineAt(cursorPos)
|
|
||||||
|
|
||||||
cachedCursor.value = {
|
|
||||||
line: line.number - 1,
|
|
||||||
ch: cursorPos - line.from,
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.value = {
|
|
||||||
line: cachedCursor.value.line,
|
|
||||||
ch: cachedCursor.value.ch,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.value = {
|
cursor.value = {
|
||||||
@@ -331,7 +313,6 @@ export function useCodemirror(
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||||
additionalExts.of(options.additionalExts ?? []),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if (environmentTooltip) extensions.push(environmentTooltip.extension)
|
if (environmentTooltip) extensions.push(environmentTooltip.extension)
|
||||||
@@ -407,15 +388,6 @@ export function useCodemirror(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => options.additionalExts,
|
|
||||||
(newExts) => {
|
|
||||||
view.value?.dispatch({
|
|
||||||
effects: additionalExts.reconfigure(newExts ?? []),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => options.extendedEditorConfig.lineWrapping,
|
() => options.extendedEditorConfig.lineWrapping,
|
||||||
(newMode) => {
|
(newMode) => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type CloneMode = "noclone" | "shallow" | "deep"
|
|||||||
*/
|
*/
|
||||||
export function useReadonlyStream<T>(
|
export function useReadonlyStream<T>(
|
||||||
stream$: Observable<T>,
|
stream$: Observable<T>,
|
||||||
initialValue?: T,
|
initialValue: T,
|
||||||
cloneMode: CloneMode = "shallow"
|
cloneMode: CloneMode = "shallow"
|
||||||
): Ref<T> {
|
): Ref<T> {
|
||||||
let sub: Subscription | null = null
|
let sub: Subscription | null = null
|
||||||
|
|||||||
289
packages/hoppscotch-common/src/helpers/GQLConnection.ts
Normal file
289
packages/hoppscotch-common/src/helpers/GQLConnection.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { BehaviorSubject } from "rxjs"
|
||||||
|
import {
|
||||||
|
getIntrospectionQuery,
|
||||||
|
buildClientSchema,
|
||||||
|
GraphQLSchema,
|
||||||
|
printSchema,
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLInputObjectType,
|
||||||
|
GraphQLEnumType,
|
||||||
|
GraphQLInterfaceType,
|
||||||
|
} from "graphql"
|
||||||
|
import { distinctUntilChanged, map } from "rxjs/operators"
|
||||||
|
import { GQLHeader, HoppGQLAuth } from "@hoppscotch/data"
|
||||||
|
import { getService } from "~/modules/dioc"
|
||||||
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
|
|
||||||
|
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||||
|
|
||||||
|
/**
|
||||||
|
GQLConnection deals with all the operations (like polling, schema extraction) that runs
|
||||||
|
when a connection is made to a GraphQL server.
|
||||||
|
*/
|
||||||
|
export class GQLConnection {
|
||||||
|
public isLoading$ = new BehaviorSubject<boolean>(false)
|
||||||
|
public connected$ = new BehaviorSubject<boolean>(false)
|
||||||
|
public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
|
||||||
|
|
||||||
|
public schemaString$ = this.schema$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((schema) => {
|
||||||
|
if (!schema) return null
|
||||||
|
|
||||||
|
return printSchema(schema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
public queryFields$ = this.schema$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((schema) => {
|
||||||
|
if (!schema) return null
|
||||||
|
|
||||||
|
const fields = schema.getQueryType()?.getFields()
|
||||||
|
if (!fields) return null
|
||||||
|
|
||||||
|
return Object.values(fields)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
public mutationFields$ = this.schema$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((schema) => {
|
||||||
|
if (!schema) return null
|
||||||
|
|
||||||
|
const fields = schema.getMutationType()?.getFields()
|
||||||
|
if (!fields) return null
|
||||||
|
|
||||||
|
return Object.values(fields)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
public subscriptionFields$ = this.schema$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((schema) => {
|
||||||
|
if (!schema) return null
|
||||||
|
|
||||||
|
const fields = schema.getSubscriptionType()?.getFields()
|
||||||
|
if (!fields) return null
|
||||||
|
|
||||||
|
return Object.values(fields)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
public graphqlTypes$ = this.schema$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((schema) => {
|
||||||
|
if (!schema) return null
|
||||||
|
|
||||||
|
const typeMap = schema.getTypeMap()
|
||||||
|
|
||||||
|
const queryTypeName = schema.getQueryType()?.name ?? ""
|
||||||
|
const mutationTypeName = schema.getMutationType()?.name ?? ""
|
||||||
|
const subscriptionTypeName = schema.getSubscriptionType()?.name ?? ""
|
||||||
|
|
||||||
|
return Object.values(typeMap).filter((type) => {
|
||||||
|
return (
|
||||||
|
!type.name.startsWith("__") &&
|
||||||
|
![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
|
||||||
|
type.name
|
||||||
|
) &&
|
||||||
|
(type instanceof GraphQLObjectType ||
|
||||||
|
type instanceof GraphQLInputObjectType ||
|
||||||
|
type instanceof GraphQLEnumType ||
|
||||||
|
type instanceof GraphQLInterfaceType)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
private timeoutSubscription: any
|
||||||
|
|
||||||
|
public connect(url: string, headers: GQLHeader[], auth: HoppGQLAuth) {
|
||||||
|
if (this.connected$.value) {
|
||||||
|
throw new Error(
|
||||||
|
"A connection is already running. Close it before starting another."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polling
|
||||||
|
this.connected$.next(true)
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
await this.getSchema(url, headers, auth)
|
||||||
|
this.timeoutSubscription = setTimeout(() => {
|
||||||
|
poll()
|
||||||
|
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect() {
|
||||||
|
if (!this.connected$.value) {
|
||||||
|
throw new Error("No connections are running to be disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(this.timeoutSubscription)
|
||||||
|
this.connected$.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
if (this.connected$.value) this.disconnect()
|
||||||
|
|
||||||
|
this.isLoading$.next(false)
|
||||||
|
this.connected$.next(false)
|
||||||
|
this.schema$.next(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSchema(
|
||||||
|
url: string,
|
||||||
|
reqHeaders: GQLHeader[],
|
||||||
|
auth: HoppGQLAuth
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.isLoading$.next(true)
|
||||||
|
|
||||||
|
const introspectionQuery = JSON.stringify({
|
||||||
|
query: getIntrospectionQuery(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers = reqHeaders.filter((x) => x.active && x.key !== "")
|
||||||
|
|
||||||
|
// TODO: Support a better b64 implementation than btoa ?
|
||||||
|
if (auth.authType === "basic") {
|
||||||
|
const username = auth.username
|
||||||
|
const password = auth.password
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key: "Authorization",
|
||||||
|
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||||
|
})
|
||||||
|
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key: "Authorization",
|
||||||
|
value: `Bearer ${auth.token}`,
|
||||||
|
})
|
||||||
|
} else if (auth.authType === "api-key") {
|
||||||
|
const { key, value, addTo } = auth
|
||||||
|
|
||||||
|
if (addTo === "Headers") {
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalHeaders: Record<string, string> = {}
|
||||||
|
headers.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||||
|
|
||||||
|
const reqOptions = {
|
||||||
|
method: "POST" as const,
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
...finalHeaders,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
data: introspectionQuery,
|
||||||
|
}
|
||||||
|
|
||||||
|
const interceptorService = getService(InterceptorService)
|
||||||
|
|
||||||
|
const res = await interceptorService.runRequest(reqOptions).response
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
console.error(res.left)
|
||||||
|
throw new Error(res.left.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = res.right
|
||||||
|
|
||||||
|
// HACK : Temporary trailing null character issue from the extension fix
|
||||||
|
const response = new TextDecoder("utf-8")
|
||||||
|
.decode(data.data as any)
|
||||||
|
.replace(/\0+$/, "")
|
||||||
|
|
||||||
|
const introspectResponse = JSON.parse(response)
|
||||||
|
|
||||||
|
const schema = buildClientSchema(introspectResponse.data)
|
||||||
|
|
||||||
|
this.schema$.next(schema)
|
||||||
|
|
||||||
|
this.isLoading$.next(false)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runQuery(
|
||||||
|
url: string,
|
||||||
|
headers: GQLHeader[],
|
||||||
|
query: string,
|
||||||
|
variables: string,
|
||||||
|
auth: HoppGQLAuth
|
||||||
|
) {
|
||||||
|
const finalHeaders: Record<string, string> = {}
|
||||||
|
|
||||||
|
const parsedVariables = JSON.parse(variables || "{}")
|
||||||
|
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
|
||||||
|
if (auth.authActive) {
|
||||||
|
if (auth.authType === "basic") {
|
||||||
|
const username = auth.username
|
||||||
|
const password = auth.password
|
||||||
|
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
|
||||||
|
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||||
|
finalHeaders.Authorization = `Bearer ${auth.token}`
|
||||||
|
} else if (auth.authType === "api-key") {
|
||||||
|
const { key, value, addTo } = auth
|
||||||
|
if (addTo === "Headers") {
|
||||||
|
finalHeaders[key] = value
|
||||||
|
} else if (addTo === "Query params") {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers
|
||||||
|
.filter((item) => item.active && item.key !== "")
|
||||||
|
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||||
|
|
||||||
|
const reqOptions = {
|
||||||
|
method: "POST" as const,
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
...finalHeaders,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
data: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: parsedVariables,
|
||||||
|
}),
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const interceptorService = getService(InterceptorService)
|
||||||
|
const result = await interceptorService.runRequest(reqOptions).response
|
||||||
|
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
console.error(result.left)
|
||||||
|
throw new Error(result.left.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = result.right
|
||||||
|
|
||||||
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
|
const responseText = new TextDecoder("utf-8")
|
||||||
|
.decode(res.data as any)
|
||||||
|
.replace(/\0+$/, "")
|
||||||
|
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import { BehaviorSubject } from "rxjs"
|
|||||||
import { HoppRESTDocument } from "./rest/document"
|
import { HoppRESTDocument } from "./rest/document"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||||
import { HoppGQLSaveContext } from "./graphql/document"
|
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||||
@@ -26,15 +25,14 @@ export type HoppAction =
|
|||||||
| "request.method.delete" // Select DELETE Method
|
| "request.method.delete" // Select DELETE Method
|
||||||
| "request.import-curl" // Import cURL
|
| "request.import-curl" // Import cURL
|
||||||
| "request.show-code" // Show generated code
|
| "request.show-code" // Show generated code
|
||||||
| "collection.new" // Create root collection
|
|
||||||
| "flyouts.chat.open" // Shows the keybinds flyout
|
| "flyouts.chat.open" // Shows the keybinds flyout
|
||||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||||
| "modals.search.toggle" // Shows the search modal
|
| "modals.search.toggle" // Shows the search modal
|
||||||
| "modals.support.toggle" // Shows the support modal
|
| "modals.support.toggle" // Shows the support modal
|
||||||
| "modals.share.toggle" // Shows the share modal
|
| "modals.share.toggle" // Shows the share modal
|
||||||
|
| "modals.social.toggle" // Shows the social links modal
|
||||||
| "modals.environment.add" // Show add environment modal via context menu
|
| "modals.environment.add" // Show add environment modal via context menu
|
||||||
| "modals.environment.new" // Add new environment
|
| "modals.environment.new" // Add new environment
|
||||||
| "modals.environment.delete-selected" // Delete Selected Environment
|
|
||||||
| "modals.my.environment.edit" // Edit current personal environment
|
| "modals.my.environment.edit" // Edit current personal environment
|
||||||
| "modals.team.environment.edit" // Edit current team environment
|
| "modals.team.environment.edit" // Edit current team environment
|
||||||
| "modals.team.new" // Add new team
|
| "modals.team.new" // Add new team
|
||||||
@@ -58,7 +56,6 @@ export type HoppAction =
|
|||||||
| "history.clear" // Clear REST History
|
| "history.clear" // Clear REST History
|
||||||
| "user.login" // Login to Hoppscotch
|
| "user.login" // Login to Hoppscotch
|
||||||
| "user.logout" // Log out of Hoppscotch
|
| "user.logout" // Log out of Hoppscotch
|
||||||
| "editor.format" // Format editor content
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the arguments, if present for a given type that is required to be passed on
|
* Defines the arguments, if present for a given type that is required to be passed on
|
||||||
@@ -109,13 +106,8 @@ type HoppActionArgsMap = {
|
|||||||
tab: RequestOptionTabs
|
tab: RequestOptionTabs
|
||||||
}
|
}
|
||||||
|
|
||||||
"request.duplicate-tab": {
|
|
||||||
tabID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
saveContext?: HoppGQLSaveContext
|
|
||||||
}
|
}
|
||||||
"modals.environment.add": {
|
"modals.environment.add": {
|
||||||
envName: string
|
envName: string
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import {
|
|||||||
getSelectedEnvironmentType,
|
getSelectedEnvironmentType,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import IconUser from "~icons/lucide/user?raw"
|
|
||||||
import IconUsers from "~icons/lucide/users?raw"
|
|
||||||
import IconEdit from "~icons/lucide/edit?raw"
|
|
||||||
|
|
||||||
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||||
|
|
||||||
@@ -74,14 +71,14 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
|
|
||||||
const selectedEnvType = getSelectedEnvironmentType()
|
const selectedEnvType = getSelectedEnvironmentType()
|
||||||
|
|
||||||
const envTypeIcon = `<span class="inline-flex items-center justify-center my-1">${
|
const envTypeIcon = `<span class="inline-flex -my-2 -mx-0.5 opacity-65 items-center text-base font-icon">${
|
||||||
selectedEnvType === "TEAM_ENV" ? IconUsers : IconUser
|
selectedEnvType === "TEAM_ENV" ? "group" : "person"
|
||||||
}</span>`
|
}</span>`
|
||||||
|
|
||||||
const appendEditAction = (tooltip: HTMLElement) => {
|
const appendEditAction = (tooltip: HTMLElement) => {
|
||||||
const editIcon = document.createElement("button")
|
const editIcon = document.createElement("span")
|
||||||
editIcon.className =
|
editIcon.className =
|
||||||
"ml-2 cursor-pointer text-accent hover:text-accentDark"
|
"ml-2 cursor-pointer env-icon text-accent hover:text-accentDark"
|
||||||
editIcon.addEventListener("click", () => {
|
editIcon.addEventListener("click", () => {
|
||||||
const isPersonalEnv =
|
const isPersonalEnv =
|
||||||
envName === "Global" || selectedEnvType !== "TEAM_ENV"
|
envName === "Global" || selectedEnvType !== "TEAM_ENV"
|
||||||
@@ -91,7 +88,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
variableName: parsedEnvKey,
|
variableName: parsedEnvKey,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
editIcon.innerHTML = `<span class="inline-flex items-center px-1 -mx-1 -my-2 text-base font-icon">edit</span>`
|
||||||
tooltip.appendChild(editIcon)
|
tooltip.appendChild(editIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +103,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
const kbd = document.createElement("kbd")
|
const kbd = document.createElement("kbd")
|
||||||
const icon = document.createElement("span")
|
const icon = document.createElement("span")
|
||||||
icon.innerHTML = envTypeIcon
|
icon.innerHTML = envTypeIcon
|
||||||
icon.className = "mr-2"
|
icon.className = "mr-2 env-icon"
|
||||||
kbd.textContent = finalEnv
|
kbd.textContent = finalEnv
|
||||||
tooltipContainer.appendChild(icon)
|
tooltipContainer.appendChild(icon)
|
||||||
tooltipContainer.appendChild(document.createTextNode(`${envName} `))
|
tooltipContainer.appendChild(document.createTextNode(`${envName} `))
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { EditorState, Range } from "@codemirror/state"
|
|
||||||
import { Decoration, ViewPlugin } from "@codemirror/view"
|
|
||||||
import { syntaxTree } from "@codemirror/language"
|
|
||||||
|
|
||||||
function getOperationDefsPosInEditor(state: EditorState) {
|
|
||||||
const tree = syntaxTree(state)
|
|
||||||
|
|
||||||
const defs: Array<{ from: number; to: number }> = []
|
|
||||||
|
|
||||||
tree.iterate({
|
|
||||||
enter({ name, from, to }) {
|
|
||||||
if (name === "OperationDefinition") {
|
|
||||||
defs.push({ from, to })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return defs
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSelectedOpDecors(state: EditorState) {
|
|
||||||
const selectedPos = state.selection.main.head // Cursor Pos
|
|
||||||
|
|
||||||
const defsPositions = getOperationDefsPosInEditor(state)
|
|
||||||
|
|
||||||
if (defsPositions.length === 1) return Decoration.none
|
|
||||||
|
|
||||||
const decors = defsPositions
|
|
||||||
.map(({ from, to }) => ({
|
|
||||||
selected: selectedPos >= from && selectedPos <= to,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
}))
|
|
||||||
.map((info) => ({
|
|
||||||
...info,
|
|
||||||
decor: Decoration.mark({
|
|
||||||
class: info.selected
|
|
||||||
? "gql-operation-highlight"
|
|
||||||
: "gql-operation-not-highlight",
|
|
||||||
inclusive: true,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.map(({ from, to, decor }) => <Range<Decoration>>{ from, to, value: decor }) // Convert to Range<Decoration> (Range from "@codemirror/view")
|
|
||||||
|
|
||||||
return Decoration.set(decors)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const selectedGQLOpHighlight = ViewPlugin.define(
|
|
||||||
(view) => ({
|
|
||||||
decorations: generateSelectedOpDecors(view.state),
|
|
||||||
update(u) {
|
|
||||||
this.decorations = generateSelectedOpDecors(u.state)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
decorations: (v) => v.decorations,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
|
|
||||||
import { OperationType } from "@urql/core"
|
|
||||||
import {
|
|
||||||
GraphQLEnumType,
|
|
||||||
GraphQLInputObjectType,
|
|
||||||
GraphQLInterfaceType,
|
|
||||||
GraphQLObjectType,
|
|
||||||
GraphQLSchema,
|
|
||||||
buildClientSchema,
|
|
||||||
getIntrospectionQuery,
|
|
||||||
printSchema,
|
|
||||||
} from "graphql"
|
|
||||||
import { computed, reactive, ref } from "vue"
|
|
||||||
import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"
|
|
||||||
import { currentTabID } from "./tab"
|
|
||||||
import { getService } from "~/modules/dioc"
|
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
|
|
||||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
|
||||||
|
|
||||||
type RunQueryOptions = {
|
|
||||||
name?: string
|
|
||||||
url: string
|
|
||||||
headers: GQLHeader[]
|
|
||||||
query: string
|
|
||||||
variables: string
|
|
||||||
auth: HoppGQLAuth
|
|
||||||
operationName: string | undefined
|
|
||||||
operationType: OperationType
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GQLResponseEvent = {
|
|
||||||
time: number
|
|
||||||
operationName: string | undefined
|
|
||||||
operationType: OperationType
|
|
||||||
data: string
|
|
||||||
rawQuery?: RunQueryOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
|
||||||
export type SubscriptionState = "SUBSCRIBING" | "SUBSCRIBED" | "UNSUBSCRIBED"
|
|
||||||
|
|
||||||
const GQL = {
|
|
||||||
CONNECTION_INIT: "connection_init",
|
|
||||||
CONNECTION_ACK: "connection_ack",
|
|
||||||
CONNECTION_ERROR: "connection_error",
|
|
||||||
CONNECTION_KEEP_ALIVE: "ka",
|
|
||||||
START: "start",
|
|
||||||
STOP: "stop",
|
|
||||||
CONNECTION_TERMINATE: "connection_terminate",
|
|
||||||
DATA: "data",
|
|
||||||
ERROR: "error",
|
|
||||||
COMPLETE: "complete",
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection = {
|
|
||||||
state: ConnectionState
|
|
||||||
subscriptionState: Map<string, SubscriptionState>
|
|
||||||
socket: WebSocket | undefined
|
|
||||||
schema: GraphQLSchema | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const connection = reactive<Connection>({
|
|
||||||
state: "DISCONNECTED",
|
|
||||||
subscriptionState: new Map<string, SubscriptionState>(),
|
|
||||||
socket: undefined,
|
|
||||||
schema: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const schema = computed(() => connection.schema)
|
|
||||||
export const subscriptionState = computed(() => {
|
|
||||||
return connection.subscriptionState.get(currentTabID.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const gqlMessageEvent = ref<GQLResponseEvent | "reset">()
|
|
||||||
|
|
||||||
export const schemaString = computed(() => {
|
|
||||||
if (!connection.schema) return ""
|
|
||||||
|
|
||||||
return printSchema(connection.schema, {
|
|
||||||
commentDescriptions: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const queryFields = computed(() => {
|
|
||||||
if (!connection.schema) return []
|
|
||||||
|
|
||||||
const fields = connection.schema.getQueryType()?.getFields()
|
|
||||||
if (!fields) return []
|
|
||||||
|
|
||||||
return Object.values(fields)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const mutationFields = computed(() => {
|
|
||||||
if (!connection.schema) return []
|
|
||||||
|
|
||||||
const fields = connection.schema.getMutationType()?.getFields()
|
|
||||||
if (!fields) return []
|
|
||||||
|
|
||||||
return Object.values(fields)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const subscriptionFields = computed(() => {
|
|
||||||
if (!connection.schema) return []
|
|
||||||
|
|
||||||
const fields = connection.schema.getSubscriptionType()?.getFields()
|
|
||||||
if (!fields) return []
|
|
||||||
|
|
||||||
return Object.values(fields)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const graphqlTypes = computed(() => {
|
|
||||||
if (!connection.schema) return []
|
|
||||||
|
|
||||||
const typeMap = connection.schema.getTypeMap()
|
|
||||||
|
|
||||||
const queryTypeName = connection.schema.getQueryType()?.name ?? ""
|
|
||||||
const mutationTypeName = connection.schema.getMutationType()?.name ?? ""
|
|
||||||
const subscriptionTypeName =
|
|
||||||
connection.schema.getSubscriptionType()?.name ?? ""
|
|
||||||
|
|
||||||
return Object.values(typeMap).filter((type) => {
|
|
||||||
return (
|
|
||||||
!type.name.startsWith("__") &&
|
|
||||||
![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
|
|
||||||
type.name
|
|
||||||
) &&
|
|
||||||
(type instanceof GraphQLObjectType ||
|
|
||||||
type instanceof GraphQLInputObjectType ||
|
|
||||||
type instanceof GraphQLEnumType ||
|
|
||||||
type instanceof GraphQLInterfaceType)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
let timeoutSubscription: any
|
|
||||||
|
|
||||||
export const connect = (url: string, headers: GQLHeader[]) => {
|
|
||||||
if (connection.state === "CONNECTED") {
|
|
||||||
throw new Error(
|
|
||||||
"A connection is already running. Close it before starting another."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Polling
|
|
||||||
connection.state = "CONNECTED"
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
await getSchema(url, headers)
|
|
||||||
timeoutSubscription = setTimeout(() => {
|
|
||||||
poll()
|
|
||||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
|
||||||
}
|
|
||||||
poll()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const disconnect = () => {
|
|
||||||
if (connection.state !== "CONNECTED") {
|
|
||||||
throw new Error("No connections are running to be disconnected")
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timeoutSubscription)
|
|
||||||
connection.state = "DISCONNECTED"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reset = () => {
|
|
||||||
if (connection.state === "CONNECTED") disconnect()
|
|
||||||
|
|
||||||
connection.state = "DISCONNECTED"
|
|
||||||
connection.schema = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchema = async (url: string, headers: GQLHeader[]) => {
|
|
||||||
try {
|
|
||||||
const introspectionQuery = JSON.stringify({
|
|
||||||
query: getIntrospectionQuery(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const finalHeaders: Record<string, string> = {}
|
|
||||||
headers
|
|
||||||
.filter((x) => x.active && x.key !== "")
|
|
||||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
|
||||||
|
|
||||||
const reqOptions = {
|
|
||||||
method: "POST",
|
|
||||||
url,
|
|
||||||
headers: {
|
|
||||||
...finalHeaders,
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
data: introspectionQuery,
|
|
||||||
}
|
|
||||||
|
|
||||||
const interceptorService = getService(InterceptorService)
|
|
||||||
|
|
||||||
const res = await interceptorService.runRequest(reqOptions).response
|
|
||||||
|
|
||||||
if (E.isLeft(res)) {
|
|
||||||
console.error(res.left)
|
|
||||||
throw new Error(res.left.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = res.right
|
|
||||||
|
|
||||||
// HACK : Temporary trailing null character issue from the extension fix
|
|
||||||
const response = new TextDecoder("utf-8")
|
|
||||||
.decode(data.data as any)
|
|
||||||
.replace(/\0+$/, "")
|
|
||||||
|
|
||||||
const introspectResponse = JSON.parse(response)
|
|
||||||
|
|
||||||
const schema = buildClientSchema(introspectResponse.data)
|
|
||||||
|
|
||||||
connection.schema = schema
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e)
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const runGQLOperation = async (options: RunQueryOptions) => {
|
|
||||||
const { url, headers, query, variables, auth, operationName, operationType } =
|
|
||||||
options
|
|
||||||
|
|
||||||
const finalHeaders: Record<string, string> = {}
|
|
||||||
|
|
||||||
const parsedVariables = JSON.parse(variables || "{}")
|
|
||||||
|
|
||||||
const params: Record<string, string> = {}
|
|
||||||
|
|
||||||
if (auth.authActive) {
|
|
||||||
if (auth.authType === "basic") {
|
|
||||||
const username = auth.username
|
|
||||||
const password = auth.password
|
|
||||||
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
|
|
||||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
|
||||||
finalHeaders.Authorization = `Bearer ${auth.token}`
|
|
||||||
} else if (auth.authType === "api-key") {
|
|
||||||
const { key, value, addTo } = auth
|
|
||||||
if (addTo === "Headers") {
|
|
||||||
finalHeaders[key] = value
|
|
||||||
} else if (addTo === "Query params") {
|
|
||||||
params[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headers
|
|
||||||
.filter((item) => item.active && item.key !== "")
|
|
||||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
|
||||||
|
|
||||||
const reqOptions = {
|
|
||||||
method: "POST",
|
|
||||||
url,
|
|
||||||
headers: {
|
|
||||||
...finalHeaders,
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
data: JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables: parsedVariables,
|
|
||||||
operationName,
|
|
||||||
}),
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operationType === "subscription") {
|
|
||||||
return runSubscription(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
const interceptorService = getService(InterceptorService)
|
|
||||||
const result = await interceptorService.runRequest(reqOptions).response
|
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
console.error(result.left)
|
|
||||||
throw new Error(result.left.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = result.right
|
|
||||||
|
|
||||||
// HACK: Temporary trailing null character issue from the extension fix
|
|
||||||
const responseText = new TextDecoder("utf-8")
|
|
||||||
.decode(res.data as any)
|
|
||||||
.replace(/\0+$/, "")
|
|
||||||
|
|
||||||
gqlMessageEvent.value = {
|
|
||||||
time: Date.now(),
|
|
||||||
operationName: operationName ?? "query",
|
|
||||||
data: responseText,
|
|
||||||
rawQuery: options,
|
|
||||||
operationType,
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryToHistory(options, responseText)
|
|
||||||
|
|
||||||
return responseText
|
|
||||||
}
|
|
||||||
|
|
||||||
export const runSubscription = (options: RunQueryOptions) => {
|
|
||||||
const { url, query, operationName } = options
|
|
||||||
const wsUrl = url.replace(/^http/, "ws")
|
|
||||||
|
|
||||||
connection.subscriptionState.set(currentTabID.value, "SUBSCRIBING")
|
|
||||||
|
|
||||||
connection.socket = new WebSocket(wsUrl, "graphql-ws")
|
|
||||||
|
|
||||||
connection.socket.onopen = (event) => {
|
|
||||||
console.log("WebSocket is open now.", event)
|
|
||||||
connection.socket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: GQL.CONNECTION_INIT,
|
|
||||||
payload: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
connection.socket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: GQL.START,
|
|
||||||
id: "1",
|
|
||||||
payload: { query, operationName },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
gqlMessageEvent.value = "reset"
|
|
||||||
|
|
||||||
connection.socket.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
switch (data.type) {
|
|
||||||
case GQL.CONNECTION_ACK: {
|
|
||||||
connection.subscriptionState.set(currentTabID.value, "SUBSCRIBED")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case GQL.CONNECTION_ERROR: {
|
|
||||||
console.error(data.payload)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case GQL.CONNECTION_KEEP_ALIVE: {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case GQL.DATA: {
|
|
||||||
gqlMessageEvent.value = {
|
|
||||||
time: Date.now(),
|
|
||||||
operationName,
|
|
||||||
data: JSON.stringify(data.payload),
|
|
||||||
operationType: "subscription",
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case GQL.COMPLETE: {
|
|
||||||
console.log("completed", data.id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.socket.onclose = (event) => {
|
|
||||||
console.log("WebSocket is closed now.", event)
|
|
||||||
connection.subscriptionState.set(currentTabID.value, "UNSUBSCRIBED")
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryToHistory(options, "")
|
|
||||||
|
|
||||||
return connection.socket
|
|
||||||
}
|
|
||||||
|
|
||||||
export const socketDisconnect = () => {
|
|
||||||
connection.socket?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const addQueryToHistory = (options: RunQueryOptions, response: string) => {
|
|
||||||
const { name, url, headers, query, variables, auth } = options
|
|
||||||
addGraphqlHistoryEntry(
|
|
||||||
makeGQLHistoryEntry({
|
|
||||||
request: makeGQLRequest({
|
|
||||||
name: name ?? "Untitled Request",
|
|
||||||
url,
|
|
||||||
query,
|
|
||||||
headers,
|
|
||||||
variables,
|
|
||||||
auth,
|
|
||||||
}),
|
|
||||||
response,
|
|
||||||
star: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { parse, print } from "graphql"
|
|
||||||
import { HoppGQLRequest, GQL_REQ_SCHEMA_VERSION } from "@hoppscotch/data"
|
|
||||||
|
|
||||||
const DEFAULT_QUERY = print(
|
|
||||||
parse(
|
|
||||||
`
|
|
||||||
query Request {
|
|
||||||
method
|
|
||||||
url
|
|
||||||
headers {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{ allowLegacyFragmentVariables: true }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const getDefaultGQLRequest = (): HoppGQLRequest => ({
|
|
||||||
v: GQL_REQ_SCHEMA_VERSION,
|
|
||||||
name: "Untitled",
|
|
||||||
url: "https://echo.hoppscotch.io/graphql",
|
|
||||||
headers: [],
|
|
||||||
variables: `{
|
|
||||||
"id": "1"
|
|
||||||
}`,
|
|
||||||
query: DEFAULT_QUERY,
|
|
||||||
auth: {
|
|
||||||
authType: "none",
|
|
||||||
authActive: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
|
||||||
|
|
||||||
export type HoppGQLSaveContext =
|
|
||||||
| {
|
|
||||||
/**
|
|
||||||
* The origin source of the request
|
|
||||||
*/
|
|
||||||
originLocation: "user-collection"
|
|
||||||
/**
|
|
||||||
* Path to the request folder
|
|
||||||
*/
|
|
||||||
folderPath: string
|
|
||||||
/**
|
|
||||||
* Index to the request
|
|
||||||
*/
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
/**
|
|
||||||
* The origin source of the request
|
|
||||||
*/
|
|
||||||
originLocation: "team-collection"
|
|
||||||
/**
|
|
||||||
* ID of the request in the team
|
|
||||||
*/
|
|
||||||
requestID: string
|
|
||||||
/**
|
|
||||||
* ID of the team
|
|
||||||
*/
|
|
||||||
teamID?: string
|
|
||||||
/**
|
|
||||||
* ID of the collection loaded
|
|
||||||
*/
|
|
||||||
collectionID?: string
|
|
||||||
}
|
|
||||||
| null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines a live 'document' (something that is open and being edited) in the app
|
|
||||||
*/
|
|
||||||
export type HoppGQLDocument = {
|
|
||||||
/**
|
|
||||||
* The request as it is in the document
|
|
||||||
*/
|
|
||||||
request: HoppGQLRequest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the request has any unsaved changes
|
|
||||||
* (atleast as far as we can say)
|
|
||||||
*/
|
|
||||||
isDirty: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Info about where this request should be saved.
|
|
||||||
* This contains where the request is originated from basically.
|
|
||||||
*/
|
|
||||||
saveContext?: HoppGQLSaveContext
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import * as Eq from "fp-ts/Eq"
|
|
||||||
import * as S from "fp-ts/string"
|
|
||||||
import isEqual from "lodash-es/isEqual"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Eq-s are fp-ts an interface (type class) that defines how the equality
|
|
||||||
* of 2 values of a certain type are matched as equal
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an Eq from a non-undefinable value and makes it accept undefined
|
|
||||||
* @param eq The non nullable Eq to add to
|
|
||||||
* @returns The updated Eq which accepts undefined
|
|
||||||
*/
|
|
||||||
export const undefinedEq = <T>(eq: Eq.Eq<T>): Eq.Eq<T | undefined> => ({
|
|
||||||
equals(x: T | undefined, y: T | undefined) {
|
|
||||||
if (x !== undefined && y !== undefined) {
|
|
||||||
return eq.equals(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
return x === undefined && y === undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Eq which compares by transforming based on a mapping function and then applying the Eq to it
|
|
||||||
* @param map The mapping function to map values to
|
|
||||||
* @param eq The Eq which takes the value which the map returns
|
|
||||||
* @returns An Eq which takes the input of the mapping function
|
|
||||||
*/
|
|
||||||
export const mapThenEq = <A, B>(map: (x: A) => B, eq: Eq.Eq<B>): Eq.Eq<A> => ({
|
|
||||||
equals(x: A, y: A) {
|
|
||||||
return eq.equals(map(x), map(y))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Eq which checks equality of 2 string in a case insensitive way
|
|
||||||
*/
|
|
||||||
export const stringCaseInsensitiveEq: Eq.Eq<string> = mapThenEq(
|
|
||||||
S.toLowerCase,
|
|
||||||
S.Eq
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Eq that does equality check with Lodash's isEqual function
|
|
||||||
*/
|
|
||||||
export const lodashIsEqualEq: Eq.Eq<any> = {
|
|
||||||
equals(x: any, y: any) {
|
|
||||||
return isEqual(x, y)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { HoppGQLRequest, ValidContentTypes } from "@hoppscotch/data"
|
|
||||||
import * as Eq from "fp-ts/Eq"
|
|
||||||
import * as N from "fp-ts/number"
|
|
||||||
import * as S from "fp-ts/string"
|
|
||||||
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "./eq"
|
|
||||||
|
|
||||||
export type HoppGQLParam = {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
active: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HoppGQLHeader = {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
active: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FormDataKeyValue = {
|
|
||||||
key: string
|
|
||||||
active: boolean
|
|
||||||
} & ({ isFile: true; value: Blob[] } | { isFile: false; value: string })
|
|
||||||
|
|
||||||
export type HoppGQLReqBodyFormData = {
|
|
||||||
contentType: "multipart/form-data"
|
|
||||||
body: FormDataKeyValue[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HoppGQLReqBody =
|
|
||||||
| {
|
|
||||||
contentType: Exclude<ValidContentTypes, "multipart/form-data">
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
| HoppGQLReqBodyFormData
|
|
||||||
| {
|
|
||||||
contentType: null
|
|
||||||
body: null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HoppGQLRequestEq = Eq.struct<HoppGQLRequest>({
|
|
||||||
id: undefinedEq(S.Eq),
|
|
||||||
v: N.Eq,
|
|
||||||
name: S.Eq,
|
|
||||||
url: S.Eq,
|
|
||||||
headers: mapThenEq(
|
|
||||||
(arr) => arr.filter((h) => h.key !== "" && h.value !== ""),
|
|
||||||
lodashIsEqualEq
|
|
||||||
),
|
|
||||||
query: S.Eq,
|
|
||||||
variables: S.Eq,
|
|
||||||
auth: lodashIsEqualEq,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const isEqualHoppGQLRequest = HoppGQLRequestEq.equals
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
import { refWithControl } from "@vueuse/core"
|
|
||||||
import { isEqual } from "lodash-es"
|
|
||||||
import { v4 as uuidV4 } from "uuid"
|
|
||||||
import { computed, reactive, ref, shallowReadonly, watch } from "vue"
|
|
||||||
import { HoppTestResult } from "../types/HoppTestResult"
|
|
||||||
import { GQLResponseEvent } from "./connection"
|
|
||||||
import { getDefaultGQLRequest } from "./default"
|
|
||||||
import { HoppGQLDocument, HoppGQLSaveContext } from "./document"
|
|
||||||
|
|
||||||
export type HoppGQLTab = {
|
|
||||||
id: string
|
|
||||||
document: HoppGQLDocument
|
|
||||||
response?: GQLResponseEvent[] | null
|
|
||||||
testResults?: HoppTestResult | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PersistableGQLTabState = {
|
|
||||||
lastActiveTabID: string
|
|
||||||
orderedDocs: Array<{
|
|
||||||
tabID: string
|
|
||||||
doc: HoppGQLDocument
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const currentTabID = refWithControl("test", {
|
|
||||||
onBeforeChange(newTabID) {
|
|
||||||
if (!newTabID || !tabMap.has(newTabID)) {
|
|
||||||
console.warn(
|
|
||||||
`Tried to set current tab id to an invalid value. (value: ${newTabID})`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Don't allow change
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabMap = reactive(
|
|
||||||
new Map<string, HoppGQLTab>([
|
|
||||||
[
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
id: "test",
|
|
||||||
document: {
|
|
||||||
request: getDefaultGQLRequest(),
|
|
||||||
isDirty: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
const tabOrdering = ref<string[]>(["test"])
|
|
||||||
|
|
||||||
watch(
|
|
||||||
tabOrdering,
|
|
||||||
(newOrdering) => {
|
|
||||||
if (!currentTabID.value || !newOrdering.includes(currentTabID.value)) {
|
|
||||||
currentTabID.value = newOrdering[newOrdering.length - 1] // newOrdering should always be non-empty
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
export const persistableTabState = computed<PersistableGQLTabState>(() => ({
|
|
||||||
lastActiveTabID: currentTabID.value,
|
|
||||||
orderedDocs: tabOrdering.value.map((tabID) => {
|
|
||||||
const tab = tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
|
|
||||||
return {
|
|
||||||
tabID: tab.id,
|
|
||||||
doc: tab.document,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const currentActiveTab = computed(() => tabMap.get(currentTabID.value)!) // Guaranteed to not be undefined
|
|
||||||
|
|
||||||
// TODO: Mark this unknown and do validations
|
|
||||||
export function loadTabsFromPersistedState(data: PersistableGQLTabState) {
|
|
||||||
if (data) {
|
|
||||||
tabMap.clear()
|
|
||||||
tabOrdering.value = []
|
|
||||||
|
|
||||||
for (const doc of data.orderedDocs) {
|
|
||||||
tabMap.set(doc.tabID, {
|
|
||||||
id: doc.tabID,
|
|
||||||
document: doc.doc,
|
|
||||||
})
|
|
||||||
|
|
||||||
tabOrdering.value.push(doc.tabID)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTabID.value = data.lastActiveTabID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all the active Tab IDs in order
|
|
||||||
*/
|
|
||||||
export function getActiveTabs() {
|
|
||||||
return shallowReadonly(
|
|
||||||
computed(() => tabOrdering.value.map((x) => tabMap.get(x)!))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTabRef(tabID: string) {
|
|
||||||
return computed({
|
|
||||||
get() {
|
|
||||||
const result = tabMap.get(tabID)
|
|
||||||
|
|
||||||
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
|
|
||||||
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
return tabMap.set(tabID, value)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateNewTabID() {
|
|
||||||
while (true) {
|
|
||||||
const id = uuidV4()
|
|
||||||
|
|
||||||
if (!tabMap.has(id)) return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTab(tabUpdate: HoppGQLTab) {
|
|
||||||
if (!tabMap.has(tabUpdate.id)) {
|
|
||||||
console.warn(
|
|
||||||
`Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tabMap.set(tabUpdate.id, tabUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNewTab(document: HoppGQLDocument, switchToIt = true) {
|
|
||||||
const id = generateNewTabID()
|
|
||||||
|
|
||||||
const tab: HoppGQLTab = { id, document }
|
|
||||||
|
|
||||||
tabMap.set(id, tab)
|
|
||||||
tabOrdering.value.push(id)
|
|
||||||
|
|
||||||
if (switchToIt) {
|
|
||||||
currentTabID.value = id
|
|
||||||
}
|
|
||||||
|
|
||||||
return tab
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTabOrdering(fromIndex: number, toIndex: number) {
|
|
||||||
tabOrdering.value.splice(
|
|
||||||
toIndex,
|
|
||||||
0,
|
|
||||||
tabOrdering.value.splice(fromIndex, 1)[0]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeTab(tabID: string) {
|
|
||||||
if (!tabMap.has(tabID)) {
|
|
||||||
console.warn(`Tried to close a tab which does not exist (tab id: ${tabID})`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabOrdering.value.length === 1) {
|
|
||||||
console.warn(
|
|
||||||
`Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
|
|
||||||
|
|
||||||
tabMap.delete(tabID)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTabRefWithSaveContext(ctx: HoppGQLSaveContext) {
|
|
||||||
for (const tab of tabMap.values()) {
|
|
||||||
// For `team-collection` request id can be considered unique
|
|
||||||
if (ctx && ctx.originLocation === "team-collection") {
|
|
||||||
if (
|
|
||||||
tab.document.saveContext?.originLocation === "team-collection" &&
|
|
||||||
tab.document.saveContext.requestID === ctx.requestID
|
|
||||||
) {
|
|
||||||
return getTabRef(tab.id)
|
|
||||||
}
|
|
||||||
} else if (isEqual(ctx, tab.document.saveContext)) return getTabRef(tab.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTabsRefTo(func: (tab: HoppGQLTab) => boolean) {
|
|
||||||
return Array.from(tabMap.values())
|
|
||||||
.filter(func)
|
|
||||||
.map((tab) => getTabRef(tab.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeOtherTabs(tabID: string) {
|
|
||||||
if (!tabMap.has(tabID)) {
|
|
||||||
console.warn(
|
|
||||||
`The tab to close other tabs does not exist (tab id: ${tabID})`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tabOrdering.value = [tabID]
|
|
||||||
|
|
||||||
tabMap.forEach((_, id) => {
|
|
||||||
if (id !== tabID) tabMap.delete(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
currentTabID.value = tabID
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDirtyTabsCount() {
|
|
||||||
let count = 0
|
|
||||||
|
|
||||||
for (const tab of tabMap.values()) {
|
|
||||||
if (tab.document.isDirty) count++
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,6 @@ export const bindings: {
|
|||||||
"ctrl-shift-p": "response.preview.toggle",
|
"ctrl-shift-p": "response.preview.toggle",
|
||||||
"ctrl-j": "response.file.download",
|
"ctrl-j": "response.file.download",
|
||||||
"ctrl-.": "response.copy",
|
"ctrl-.": "response.copy",
|
||||||
"ctrl-shift-l": "editor.format",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as HTTPSnippet from "httpsnippet"
|
import { HTTPSnippet } from "httpsnippet"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
@@ -208,10 +208,7 @@ export const generateCode = (
|
|||||||
}).convert(codegenInfo.lang, codegenInfo.mode, {
|
}).convert(codegenInfo.lang, codegenInfo.mode, {
|
||||||
indent: " ",
|
indent: " ",
|
||||||
}),
|
}),
|
||||||
(e) => {
|
(e) => e
|
||||||
console.error(e)
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Only allow string output to pass through, else none
|
// Only allow string output to pass through, else none
|
||||||
|
|||||||
278
packages/hoppscotch-common/src/newstore/GQLSession.ts
Normal file
278
packages/hoppscotch-common/src/newstore/GQLSession.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { distinctUntilChanged, pluck } from "rxjs/operators"
|
||||||
|
import {
|
||||||
|
GQLHeader,
|
||||||
|
HoppGQLRequest,
|
||||||
|
makeGQLRequest,
|
||||||
|
HoppGQLAuth,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||||
|
import { useStream } from "@composables/stream"
|
||||||
|
|
||||||
|
type GQLSession = {
|
||||||
|
request: HoppGQLRequest
|
||||||
|
schema: string
|
||||||
|
response: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultGQLSession: GQLSession = {
|
||||||
|
request: makeGQLRequest({
|
||||||
|
name: "Untitled request",
|
||||||
|
url: "https://echo.hoppscotch.io/graphql",
|
||||||
|
headers: [],
|
||||||
|
variables: `{
|
||||||
|
"id": "1"
|
||||||
|
}`,
|
||||||
|
query: `query Request {
|
||||||
|
method
|
||||||
|
url
|
||||||
|
headers {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
auth: {
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
schema: "",
|
||||||
|
response: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatchers = defineDispatchers({
|
||||||
|
setSession(_: GQLSession, { session }: { session: GQLSession }) {
|
||||||
|
return session
|
||||||
|
},
|
||||||
|
setName(curr: GQLSession, { newName }: { newName: string }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
name: newName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setURL(curr: GQLSession, { newURL }: { newURL: string }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
url: newURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setHeaders(curr: GQLSession, { headers }: { headers: GQLHeader[] }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addHeader(curr: GQLSession, { header }: { header: GQLHeader }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
headers: [...curr.request.headers, header],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeHeader(curr: GQLSession, { headerIndex }: { headerIndex: number }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
headers: curr.request.headers.filter((_x, i) => i !== headerIndex),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateHeader(
|
||||||
|
curr: GQLSession,
|
||||||
|
{
|
||||||
|
headerIndex,
|
||||||
|
updatedHeader,
|
||||||
|
}: { headerIndex: number; updatedHeader: GQLHeader }
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
headers: curr.request.headers.map((x, i) =>
|
||||||
|
i === headerIndex ? updatedHeader : x
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setQuery(curr: GQLSession, { newQuery }: { newQuery: string }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
query: newQuery,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setVariables(curr: GQLSession, { newVariables }: { newVariables: string }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
variables: newVariables,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setResponse(_: GQLSession, { newResponse }: { newResponse: string }) {
|
||||||
|
return {
|
||||||
|
response: newResponse,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setAuth(curr: GQLSession, { newAuth }: { newAuth: HoppGQLAuth }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
...curr.request,
|
||||||
|
auth: newAuth,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const gqlSessionStore = new DispatchingStore(
|
||||||
|
defaultGQLSession,
|
||||||
|
dispatchers
|
||||||
|
)
|
||||||
|
|
||||||
|
export function setGQLURL(newURL: string) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setURL",
|
||||||
|
payload: {
|
||||||
|
newURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLHeaders(headers: GQLHeader[]) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setHeaders",
|
||||||
|
payload: {
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addGQLHeader(header: GQLHeader) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "addHeader",
|
||||||
|
payload: {
|
||||||
|
header,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGQLHeader(headerIndex: number, updatedHeader: GQLHeader) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "updateHeader",
|
||||||
|
payload: {
|
||||||
|
headerIndex,
|
||||||
|
updatedHeader,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeGQLHeader(headerIndex: number) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "removeHeader",
|
||||||
|
payload: {
|
||||||
|
headerIndex,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearGQLHeaders() {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setHeaders",
|
||||||
|
payload: {
|
||||||
|
headers: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLQuery(newQuery: string) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setQuery",
|
||||||
|
payload: {
|
||||||
|
newQuery,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLVariables(newVariables: string) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setVariables",
|
||||||
|
payload: {
|
||||||
|
newVariables,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLResponse(newResponse: string) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setResponse",
|
||||||
|
payload: {
|
||||||
|
newResponse,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGQLSession() {
|
||||||
|
return gqlSessionStore.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLSession(session: GQLSession) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setSession",
|
||||||
|
payload: {
|
||||||
|
session,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGQLRequestName() {
|
||||||
|
return useStream(gqlName$, gqlSessionStore.value.request.name, (newName) => {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setName",
|
||||||
|
payload: { newName },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGQLAuth(newAuth: HoppGQLAuth) {
|
||||||
|
gqlSessionStore.dispatch({
|
||||||
|
dispatcher: "setAuth",
|
||||||
|
payload: {
|
||||||
|
newAuth,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gqlName$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "name"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
export const gqlURL$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "url"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
export const gqlQuery$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "query"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
export const gqlVariables$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "variables"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
export const gqlHeaders$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "headers"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const gqlResponse$ = gqlSessionStore.subject$.pipe(
|
||||||
|
pluck("response"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const gqlAuth$ = gqlSessionStore.subject$.pipe(pluck("request", "auth"))
|
||||||
@@ -427,10 +427,6 @@ export function getCurrentEnvironment(): Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedEnvironmentIndex() {
|
|
||||||
return environmentsStore.value.selectedEnvironmentIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSelectedEnvironmentType() {
|
export function getSelectedEnvironmentType() {
|
||||||
return environmentsStore.value.selectedEnvironmentIndex.type
|
return environmentsStore.value.selectedEnvironmentIndex.type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,8 @@ import {
|
|||||||
loadTabsFromPersistedState,
|
loadTabsFromPersistedState,
|
||||||
persistableTabState,
|
persistableTabState,
|
||||||
} from "~/helpers/rest/tab"
|
} from "~/helpers/rest/tab"
|
||||||
import {
|
import { debounceTime } from "rxjs"
|
||||||
loadTabsFromPersistedState as loadGQLTabsFromPersistedState,
|
import { gqlSessionStore, setGQLSession } from "./GQLSession"
|
||||||
persistableTabState as persistableGQLTabState,
|
|
||||||
} from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
function checkAndMigrateOldSettings() {
|
function checkAndMigrateOldSettings() {
|
||||||
if (window.localStorage.getItem("selectedEnvIndex")) {
|
if (window.localStorage.getItem("selectedEnvIndex")) {
|
||||||
@@ -342,27 +340,26 @@ export function setupRESTTabsPersistence() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupGQLTabsPersistence() {
|
// temporary persistence for GQL session
|
||||||
|
export function setupGQLPersistence() {
|
||||||
try {
|
try {
|
||||||
const state = window.localStorage.getItem("gqlTabState")
|
const state = window.localStorage.getItem("gqlState")
|
||||||
if (state) {
|
if (state) {
|
||||||
const data = JSON.parse(state)
|
const data = JSON.parse(state)
|
||||||
loadGQLTabsFromPersistedState(data)
|
data["schema"] = ""
|
||||||
|
data["response"] = ""
|
||||||
|
setGQLSession(data)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed parsing persisted tab state, state:`,
|
`Failed parsing persisted GraphQL state, state:`,
|
||||||
window.localStorage.getItem("gqlTabState")
|
window.localStorage.getItem("gqlState")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDebounced(
|
gqlSessionStore.subject$.pipe(debounceTime(500)).subscribe((state) => {
|
||||||
persistableGQLTabState,
|
window.localStorage.setItem("gqlState", JSON.stringify(state))
|
||||||
(state) => {
|
})
|
||||||
window.localStorage.setItem("gqlTabState", JSON.stringify(state))
|
|
||||||
},
|
|
||||||
{ debounce: 500, deep: true }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupLocalPersistence() {
|
export function setupLocalPersistence() {
|
||||||
@@ -371,9 +368,7 @@ export function setupLocalPersistence() {
|
|||||||
setupLocalStatePersistence()
|
setupLocalStatePersistence()
|
||||||
setupSettingsPersistence()
|
setupSettingsPersistence()
|
||||||
setupRESTTabsPersistence()
|
setupRESTTabsPersistence()
|
||||||
|
setupGQLPersistence()
|
||||||
setupGQLTabsPersistence()
|
|
||||||
|
|
||||||
setupHistoryPersistence()
|
setupHistoryPersistence()
|
||||||
setupCollectionsPersistence()
|
setupCollectionsPersistence()
|
||||||
setupGlobalEnvsPersistence()
|
setupGlobalEnvsPersistence()
|
||||||
|
|||||||
@@ -1,221 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<AppPaneLayout layout-id="graphql">
|
||||||
<AppPaneLayout layout-id="graphql">
|
<template #primary>
|
||||||
<template #primary>
|
<GraphqlRequest :conn="gqlConn" />
|
||||||
<GraphqlRequest />
|
<GraphqlRequestOptions :conn="gqlConn" />
|
||||||
|
</template>
|
||||||
<HoppSmartWindows
|
<template #secondary>
|
||||||
v-if="currentTabID"
|
<GraphqlResponse :conn="gqlConn" />
|
||||||
:id="'gql_windows'"
|
</template>
|
||||||
v-model="currentTabID"
|
<template #sidebar>
|
||||||
@remove-tab="removeTab"
|
<GraphqlSidebar :conn="gqlConn" />
|
||||||
@add-tab="addNewTab"
|
</template>
|
||||||
@sort="sortTabs"
|
</AppPaneLayout>
|
||||||
>
|
|
||||||
<HoppSmartWindow
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:id="tab.id"
|
|
||||||
:key="'removable_tab_' + tab.id"
|
|
||||||
:label="tab.document.request.name"
|
|
||||||
:is-removable="tabs.length > 1"
|
|
||||||
:close-visibility="'hover'"
|
|
||||||
>
|
|
||||||
<template #tabhead>
|
|
||||||
<GraphqlTabHead
|
|
||||||
:tab="tab"
|
|
||||||
:is-removable="tabs.length > 1"
|
|
||||||
@open-rename-modal="openReqRenameModal(tab)"
|
|
||||||
@close-tab="removeTab(tab.id)"
|
|
||||||
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
|
||||||
@duplicate-tab="duplicateTab(tab)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #suffix>
|
|
||||||
<span
|
|
||||||
v-if="tab.document.isDirty"
|
|
||||||
class="flex items-center justify-center text-secondary group-hover:hidden w-4"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="1.2em"
|
|
||||||
height="1.2em"
|
|
||||||
class="h-1.5 w-1.5"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="12" fill="currentColor"></circle>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<GraphqlRequestTab
|
|
||||||
:model-value="tab"
|
|
||||||
@update:model-value="onTabUpdate"
|
|
||||||
/>
|
|
||||||
</HoppSmartWindow>
|
|
||||||
</HoppSmartWindows>
|
|
||||||
</template>
|
|
||||||
<template #sidebar>
|
|
||||||
<GraphqlSidebar />
|
|
||||||
</template>
|
|
||||||
</AppPaneLayout>
|
|
||||||
<CollectionsEditRequest
|
|
||||||
v-model="editReqModalReqName"
|
|
||||||
:show="showRenamingReqNameModalForTabID !== undefined"
|
|
||||||
@submit="renameReqName"
|
|
||||||
@hide-modal="showRenamingReqNameModalForTabID = undefined"
|
|
||||||
/>
|
|
||||||
<HoppSmartConfirmModal
|
|
||||||
:show="confirmingCloseForTabID !== null"
|
|
||||||
:confirm="t('modal.close_unsaved_tab')"
|
|
||||||
:title="t('confirm.close_unsaved_tab')"
|
|
||||||
@hide-modal="onCloseConfirm"
|
|
||||||
@resolve="onResolveConfirm"
|
|
||||||
/>
|
|
||||||
<HoppSmartConfirmModal
|
|
||||||
:show="confirmingCloseAllTabs"
|
|
||||||
:confirm="t('modal.close_unsaved_tab')"
|
|
||||||
:title="t('confirm.close_unsaved_tabs', { count: unsavedTabsCount })"
|
|
||||||
@hide-modal="confirmingCloseAllTabs = false"
|
|
||||||
@resolve="onResolveConfirmCloseAllTabs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePageHead } from "@composables/head"
|
import { usePageHead } from "@composables/head"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useService } from "dioc/vue"
|
import { GQLConnection } from "@helpers/GQLConnection"
|
||||||
import { computed, onBeforeUnmount, ref } from "vue"
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { computed, onBeforeUnmount } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { connection, disconnect } from "~/helpers/graphql/connection"
|
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||||
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
|
||||||
import {
|
|
||||||
HoppGQLTab,
|
|
||||||
closeOtherTabs,
|
|
||||||
closeTab,
|
|
||||||
createNewTab,
|
|
||||||
currentTabID,
|
|
||||||
getActiveTabs,
|
|
||||||
getDirtyTabsCount,
|
|
||||||
getTabRef,
|
|
||||||
updateTab,
|
|
||||||
updateTabOrdering,
|
|
||||||
} from "~/helpers/graphql/tab"
|
|
||||||
import { InspectionService } from "~/services/inspection"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
|
||||||
|
|
||||||
const confirmingCloseForTabID = ref<string | null>(null)
|
|
||||||
|
|
||||||
usePageHead({
|
usePageHead({
|
||||||
title: computed(() => t("navigation.graphql")),
|
title: computed(() => t("navigation.graphql")),
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs = getActiveTabs()
|
const gqlConn = new GQLConnection()
|
||||||
|
|
||||||
const addNewTab = () => {
|
|
||||||
const tab = createNewTab({
|
|
||||||
request: getDefaultGQLRequest(),
|
|
||||||
isDirty: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
currentTabID.value = tab.id
|
|
||||||
}
|
|
||||||
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
|
||||||
updateTabOrdering(e.oldIndex, e.newIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeTab = (tabID: string) => {
|
|
||||||
const tabState = getTabRef(tabID).value
|
|
||||||
|
|
||||||
if (tabState.document.isDirty) {
|
|
||||||
confirmingCloseForTabID.value = tabID
|
|
||||||
} else {
|
|
||||||
closeTab(tabState.id)
|
|
||||||
inspectionService.deleteTabInspectorResult(tabState.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is closed when the confirm tab is closed by some means (even saving triggers close)
|
|
||||||
*/
|
|
||||||
const onCloseConfirm = () => {
|
|
||||||
confirmingCloseForTabID.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the user confirms they want to save the tab
|
|
||||||
*/
|
|
||||||
const onResolveConfirm = () => {
|
|
||||||
if (confirmingCloseForTabID.value) {
|
|
||||||
closeTab(confirmingCloseForTabID.value)
|
|
||||||
confirmingCloseForTabID.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmingCloseAllTabs = ref(false)
|
|
||||||
const unsavedTabsCount = ref(0)
|
|
||||||
const exceptedTabID = ref<string | null>(null)
|
|
||||||
|
|
||||||
const closeOtherTabsAction = (tabID: string) => {
|
|
||||||
const dirtyTabCount = getDirtyTabsCount()
|
|
||||||
// If there are dirty tabs, show the confirm modal
|
|
||||||
if (dirtyTabCount > 0) {
|
|
||||||
confirmingCloseAllTabs.value = true
|
|
||||||
unsavedTabsCount.value = dirtyTabCount
|
|
||||||
exceptedTabID.value = tabID
|
|
||||||
} else {
|
|
||||||
closeOtherTabs(tabID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onResolveConfirmCloseAllTabs = () => {
|
|
||||||
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
|
|
||||||
confirmingCloseAllTabs.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTabUpdate = (tab: HoppGQLTab) => {
|
|
||||||
updateTab(tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (connection.state === "CONNECTED") {
|
if (gqlConn.connected$.value) {
|
||||||
disconnect()
|
gqlConn.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const editReqModalReqName = ref("")
|
defineActionHandler("gql.request.open", ({ request }) => {
|
||||||
const showRenamingReqNameModalForTabID = ref<string>()
|
const session = getGQLSession()
|
||||||
|
|
||||||
const openReqRenameModal = (tab: HoppGQLTab) => {
|
setGQLSession({
|
||||||
editReqModalReqName.value = tab.document.request.name
|
request: cloneDeep(request),
|
||||||
showRenamingReqNameModalForTabID.value = tab.id
|
schema: session.schema,
|
||||||
}
|
response: session.response,
|
||||||
|
|
||||||
const renameReqName = () => {
|
|
||||||
const tab = getTabRef(showRenamingReqNameModalForTabID.value!)
|
|
||||||
if (tab.value) {
|
|
||||||
tab.value.document.request.name = editReqModalReqName.value
|
|
||||||
updateTab(tab.value)
|
|
||||||
}
|
|
||||||
showRenamingReqNameModalForTabID.value = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateTab = (tab: HoppGQLTab) => {
|
|
||||||
const newTab = createNewTab({
|
|
||||||
request: tab.document.request,
|
|
||||||
isDirty: true,
|
|
||||||
})
|
|
||||||
currentTabID.value = newTab.id
|
|
||||||
}
|
|
||||||
|
|
||||||
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
|
||||||
createNewTab({
|
|
||||||
saveContext,
|
|
||||||
request: request,
|
|
||||||
isDirty: false,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ const showRenamingReqNameModal = ref(false)
|
|||||||
const reqName = ref<string>("")
|
const reqName = ref<string>("")
|
||||||
const unsavedTabsCount = ref(0)
|
const unsavedTabsCount = ref(0)
|
||||||
const exceptedTabID = ref<string | null>(null)
|
const exceptedTabID = ref<string | null>(null)
|
||||||
const renameTabID = ref<string | null>(null)
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -258,7 +257,6 @@ const openReqRenameModal = (tabID?: string) => {
|
|||||||
if (tabID) {
|
if (tabID) {
|
||||||
const tab = getTabRef(tabID)
|
const tab = getTabRef(tabID)
|
||||||
reqName.value = tab.value.document.request.name
|
reqName.value = tab.value.document.request.name
|
||||||
renameTabID.value = tabID
|
|
||||||
} else {
|
} else {
|
||||||
reqName.value = currentActiveTab.value.document.request.name
|
reqName.value = currentActiveTab.value.document.request.name
|
||||||
}
|
}
|
||||||
@@ -266,7 +264,7 @@ const openReqRenameModal = (tabID?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renameReqName = () => {
|
const renameReqName = () => {
|
||||||
const tab = getTabRef(renameTabID.value ?? currentTabID.value)
|
const tab = getTabRef(currentTabID.value)
|
||||||
if (tab.value) {
|
if (tab.value) {
|
||||||
tab.value.document.request.name = reqName.value
|
tab.value.document.request.name = reqName.value
|
||||||
updateTab(tab.value)
|
updateTab(tab.value)
|
||||||
@@ -460,9 +458,11 @@ defineActionHandler("rest.request.open", ({ doc }) => {
|
|||||||
createNewTab(doc)
|
createNewTab(doc)
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("rest.request.rename", openReqRenameModal)
|
defineActionHandler("rest.request.rename", () => {
|
||||||
defineActionHandler("request.duplicate-tab", ({ tabID }) => {
|
// TODO: Fix this hack to open the modal
|
||||||
duplicateTab(tabID)
|
setTimeout(() => {
|
||||||
|
openReqRenameModal()
|
||||||
|
}, 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
const inspectionService = useService(InspectionService)
|
||||||
|
|||||||
@@ -16,13 +16,14 @@
|
|||||||
>
|
>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="t('auth.login')"
|
:label="t('auth.login')"
|
||||||
|
class="mb-4"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-8">
|
||||||
<div
|
<div
|
||||||
class="h-24 rounded bg-primaryLight -mb-11 md:h-32"
|
class="h-24 rounded bg-primaryLight -mb-11 md:h-32"
|
||||||
style="background-image: url(/images/cover.svg)"
|
style="background-image: url("/images/cover.svg")"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex flex-col justify-between px-4 space-y-8 md:flex-row">
|
<div class="flex flex-col justify-between px-4 space-y-8 md:flex-row">
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
@@ -101,7 +102,6 @@
|
|||||||
</label>
|
</label>
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="displayName"
|
v-model="displayName"
|
||||||
:autofocus="false"
|
|
||||||
styles="mt-2 md:max-w-sm"
|
styles="mt-2 md:max-w-sm"
|
||||||
:placeholder="`${t('settings.profile_name')}`"
|
:placeholder="`${t('settings.profile_name')}`"
|
||||||
>
|
>
|
||||||
@@ -124,7 +124,6 @@
|
|||||||
</label>
|
</label>
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="emailAddress"
|
v-model="emailAddress"
|
||||||
:autofocus="false"
|
|
||||||
styles="flex mt-2 md:max-w-sm"
|
styles="flex mt-2 md:max-w-sm"
|
||||||
:placeholder="`${t('settings.profile_name')}`"
|
:placeholder="`${t('settings.profile_name')}`"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
:label="tab.name"
|
:label="tab.name"
|
||||||
:is-removable="tab.removable"
|
:is-removable="tab.removable"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #icon>
|
||||||
<icon-lucide-rss
|
<icon-lucide-rss
|
||||||
:style="{
|
:style="{
|
||||||
color: tab.color,
|
color: tab.color,
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
blank
|
blank
|
||||||
:icon="IconExternalLink"
|
:icon="IconExternalLink"
|
||||||
reverse
|
reverse
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="url"
|
v-model="url"
|
||||||
type="url"
|
type="url"
|
||||||
:autofocus="false"
|
|
||||||
styles="!inline-flex flex-1 space-x-2"
|
styles="!inline-flex flex-1 space-x-2"
|
||||||
input-styles="w-full px-4 py-2 border rounded !bg-primaryLight border-divider text-secondaryDark"
|
input-styles="w-full px-4 py-2 border rounded !bg-primaryLight border-divider text-secondaryDark"
|
||||||
:placeholder="`${t('websocket.url')}`"
|
:placeholder="`${t('websocket.url')}`"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class ParameterMenuService extends Service implements ContextMenu {
|
|||||||
id: "environment",
|
id: "environment",
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: this.t("context_menu.add_parameters"),
|
text: this.t("context_menu.add_parameter"),
|
||||||
},
|
},
|
||||||
icon: markRaw(IconArrowDownRight),
|
icon: markRaw(IconArrowDownRight),
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export class URLMenuService extends Service implements ContextMenu {
|
|||||||
id: "link-tab",
|
id: "link-tab",
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: this.t("context_menu.open_request_in_new_tab"),
|
text: this.t("context_menu.open_link_in_new_tab"),
|
||||||
},
|
},
|
||||||
icon: markRaw(IconCopyPlus),
|
icon: markRaw(IconCopyPlus),
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SpotlightService } from "../.."
|
|||||||
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { HoppAction, HoppActionWithArgs } from "~/helpers/actions"
|
import { HoppAction, HoppActionWithArgs } from "~/helpers/actions"
|
||||||
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
import { defaultGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
async function flushPromises() {
|
async function flushPromises() {
|
||||||
return await new Promise((r) => setTimeout(r))
|
return await new Promise((r) => setTimeout(r))
|
||||||
@@ -230,7 +230,7 @@ describe("HistorySpotlightSearcherService", () => {
|
|||||||
|
|
||||||
historyMock.gqlEntries.push({
|
historyMock.gqlEntries.push({
|
||||||
request: {
|
request: {
|
||||||
...getDefaultGQLRequest(),
|
...defaultGQLSession.request,
|
||||||
url: "bla.com",
|
url: "bla.com",
|
||||||
},
|
},
|
||||||
response: "{}",
|
response: "{}",
|
||||||
@@ -267,7 +267,7 @@ describe("HistorySpotlightSearcherService", () => {
|
|||||||
|
|
||||||
const historyEntry: GQLHistoryEntry = {
|
const historyEntry: GQLHistoryEntry = {
|
||||||
request: {
|
request: {
|
||||||
...getDefaultGQLRequest(),
|
...defaultGQLSession.request,
|
||||||
url: "bla.com",
|
url: "bla.com",
|
||||||
},
|
},
|
||||||
response: "{}",
|
response: "{}",
|
||||||
@@ -302,7 +302,7 @@ describe("HistorySpotlightSearcherService", () => {
|
|||||||
|
|
||||||
historyMock.gqlEntries.push({
|
historyMock.gqlEntries.push({
|
||||||
request: {
|
request: {
|
||||||
...getDefaultGQLRequest(),
|
...defaultGQLSession.request,
|
||||||
url: "bla.com",
|
url: "bla.com",
|
||||||
},
|
},
|
||||||
response: "{}",
|
response: "{}",
|
||||||
@@ -351,7 +351,7 @@ describe("HistorySpotlightSearcherService", () => {
|
|||||||
|
|
||||||
historyMock.gqlEntries.push({
|
historyMock.gqlEntries.push({
|
||||||
request: {
|
request: {
|
||||||
...getDefaultGQLRequest(),
|
...defaultGQLSession.request,
|
||||||
url: "bla.com",
|
url: "bla.com",
|
||||||
},
|
},
|
||||||
response: "{}",
|
response: "{}",
|
||||||
@@ -398,7 +398,7 @@ describe("HistorySpotlightSearcherService", () => {
|
|||||||
it("none of the history entries are show when neither of the open actions are registered", async () => {
|
it("none of the history entries are show when neither of the open actions are registered", async () => {
|
||||||
historyMock.gqlEntries.push({
|
historyMock.gqlEntries.push({
|
||||||
request: {
|
request: {
|
||||||
...getDefaultGQLRequest(),
|
...defaultGQLSession.request,
|
||||||
url: "bla.com",
|
url: "bla.com",
|
||||||
},
|
},
|
||||||
response: "{}",
|
response: "{}",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
import {
|
import {
|
||||||
SpotlightResultTextType,
|
|
||||||
SpotlightSearcher,
|
SpotlightSearcher,
|
||||||
SpotlightSearcherResult,
|
SpotlightSearcherResult,
|
||||||
SpotlightSearcherSessionState,
|
SpotlightSearcherSessionState,
|
||||||
@@ -17,7 +16,6 @@ import IconFolder from "~icons/lucide/folder"
|
|||||||
import RESTRequestSpotlightEntry from "~/components/app/spotlight/entry/RESTRequest.vue"
|
import RESTRequestSpotlightEntry from "~/components/app/spotlight/entry/RESTRequest.vue"
|
||||||
import GQLRequestSpotlightEntry from "~/components/app/spotlight/entry/GQLRequest.vue"
|
import GQLRequestSpotlightEntry from "~/components/app/spotlight/entry/GQLRequest.vue"
|
||||||
import { createNewTab } from "~/helpers/rest/tab"
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
import { createNewTab as createNewGQLTab } from "~/helpers/graphql/tab"
|
|
||||||
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
|
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
|
||||||
import { currentTabID } from "~/helpers/rest/tab"
|
import { currentTabID } from "~/helpers/rest/tab"
|
||||||
import {
|
import {
|
||||||
@@ -25,9 +23,10 @@ import {
|
|||||||
HoppGQLRequest,
|
HoppGQLRequest,
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
|
import { setGQLSession } from "~/newstore/GQLSession"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
import { hoppWorkspaceStore } from "~/newstore/workspace"
|
import { hoppWorkspaceStore } from "~/newstore/workspace"
|
||||||
import { changeWorkspace } from "~/newstore/workspace"
|
import { changeWorkspace } from "~/newstore/workspace"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A spotlight searcher that searches through the user's collections
|
* A spotlight searcher that searches through the user's collections
|
||||||
@@ -145,13 +144,6 @@ export class CollectionsSpotlightSearcherService
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (pageCategory === "rest" || pageCategory === "graphql") {
|
|
||||||
minisearch.add({
|
|
||||||
id: `create-collection`,
|
|
||||||
name: this.t("collection.new"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageCategory === "rest") {
|
if (pageCategory === "rest") {
|
||||||
this.loadRESTDocsIntoMinisearch(minisearch)
|
this.loadRESTDocsIntoMinisearch(minisearch)
|
||||||
} else if (pageCategory === "graphql") {
|
} else if (pageCategory === "graphql") {
|
||||||
@@ -162,11 +154,6 @@ export class CollectionsSpotlightSearcherService
|
|||||||
|
|
||||||
const scopeHandle = effectScope()
|
const scopeHandle = effectScope()
|
||||||
|
|
||||||
const newCollectionText: SpotlightResultTextType<any> = {
|
|
||||||
type: "text",
|
|
||||||
text: this.t("collection.new"),
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeHandle.run(() => {
|
scopeHandle.run(() => {
|
||||||
watch(query, (query) => {
|
watch(query, (query) => {
|
||||||
if (pageCategory === "other") {
|
if (pageCategory === "other") {
|
||||||
@@ -179,34 +166,28 @@ export class CollectionsSpotlightSearcherService
|
|||||||
|
|
||||||
results.value = searchResults.map((result) => ({
|
results.value = searchResults.map((result) => ({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
text:
|
text: {
|
||||||
result.id === "create-collection"
|
type: "custom",
|
||||||
? newCollectionText
|
component: markRaw(RESTRequestSpotlightEntry),
|
||||||
: {
|
componentProps: {
|
||||||
type: "custom",
|
folderPath: result.id.split("rest-")[1],
|
||||||
component: markRaw(RESTRequestSpotlightEntry),
|
},
|
||||||
componentProps: {
|
},
|
||||||
folderPath: result.id.split("rest-")[1],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon: markRaw(IconFolder),
|
icon: markRaw(IconFolder),
|
||||||
score: result.score,
|
score: result.score,
|
||||||
}))
|
}))
|
||||||
} else if (pageCategory === "graphql") {
|
} else {
|
||||||
const searchResults = minisearch.search(query).slice(0, 10)
|
const searchResults = minisearch.search(query).slice(0, 10)
|
||||||
|
|
||||||
results.value = searchResults.map((result) => ({
|
results.value = searchResults.map((result) => ({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
text:
|
text: {
|
||||||
result.id === "create-collection"
|
type: "custom",
|
||||||
? newCollectionText
|
component: markRaw(GQLRequestSpotlightEntry),
|
||||||
: {
|
componentProps: {
|
||||||
type: "custom",
|
folderPath: result.id.split("gql-")[1],
|
||||||
component: markRaw(GQLRequestSpotlightEntry),
|
},
|
||||||
componentProps: {
|
},
|
||||||
folderPath: result.id.split("gql-")[1],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon: markRaw(IconFolder),
|
icon: markRaw(IconFolder),
|
||||||
score: result.score,
|
score: result.score,
|
||||||
}))
|
}))
|
||||||
@@ -276,8 +257,6 @@ export class CollectionsSpotlightSearcherService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onResultSelect(result: SpotlightSearcherResult): void {
|
public onResultSelect(result: SpotlightSearcherResult): void {
|
||||||
if (result.id === "create-collection") return invokeAction("collection.new")
|
|
||||||
|
|
||||||
const [type, path] = result.id.split("-")
|
const [type, path] = result.id.split("-")
|
||||||
|
|
||||||
if (type === "rest") {
|
if (type === "rest") {
|
||||||
@@ -326,14 +305,10 @@ export class CollectionsSpotlightSearcherService
|
|||||||
|
|
||||||
if (!req) return
|
if (!req) return
|
||||||
|
|
||||||
createNewGQLTab({
|
setGQLSession({
|
||||||
saveContext: {
|
request: cloneDeep(req),
|
||||||
originLocation: "user-collection",
|
schema: "",
|
||||||
folderPath: folderPath.join("/"),
|
response: "",
|
||||||
requestIndex: reqIndex,
|
|
||||||
},
|
|
||||||
request: req,
|
|
||||||
isDirty: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,29 +21,30 @@ import {
|
|||||||
StaticSpotlightSearcherService,
|
StaticSpotlightSearcherService,
|
||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconLayers from "~icons/lucide/layers"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconLayers from "~icons/lucide/layers"
|
||||||
|
|
||||||
import { Service } from "dioc"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import MiniSearch from "minisearch"
|
|
||||||
import { map } from "rxjs"
|
|
||||||
import { useStreamStatic } from "~/composables/stream"
|
import { useStreamStatic } from "~/composables/stream"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
|
||||||
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
|
||||||
import {
|
import {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
currentEnvironment$,
|
currentEnvironment$,
|
||||||
|
deleteEnvironment,
|
||||||
duplicateEnvironment,
|
duplicateEnvironment,
|
||||||
environmentsStore,
|
environmentsStore,
|
||||||
getGlobalVariables,
|
getGlobalVariables,
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||||
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { Service } from "dioc"
|
||||||
|
import MiniSearch from "minisearch"
|
||||||
|
import { map } from "rxjs"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string
|
text: string
|
||||||
@@ -70,9 +71,7 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
|
|||||||
|
|
||||||
private selectedEnvIndex = useStreamStatic(
|
private selectedEnvIndex = useStreamStatic(
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
{
|
null,
|
||||||
type: "NO_ENV_SELECTED",
|
|
||||||
},
|
|
||||||
() => {
|
() => {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
@@ -187,6 +186,29 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSelectedEnvironment = () => {
|
||||||
|
if (this.selectedEnvIndex.value?.type === "NO_ENV_SELECTED") return
|
||||||
|
|
||||||
|
if (this.selectedEnvIndex.value?.type === "MY_ENV") {
|
||||||
|
deleteEnvironment(this.selectedEnvIndex.value.index)
|
||||||
|
// this.toast.success(`${t("state.deleted")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedEnvIndex.value?.type === "TEAM_ENV") {
|
||||||
|
pipe(
|
||||||
|
deleteTeamEnvironment(this.selectedEnvIndex.value.teamEnvID),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// this.toast.success(`${this.t("team_environment.deleted")}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "new_environment":
|
case "new_environment":
|
||||||
@@ -205,7 +227,7 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "delete_selected_env":
|
case "delete_selected_env":
|
||||||
invokeAction(`modals.environment.delete-selected`)
|
this.removeSelectedEnvironment()
|
||||||
break
|
break
|
||||||
case "duplicate_selected_env":
|
case "duplicate_selected_env":
|
||||||
this.duplicateSelectedEnv()
|
this.duplicateSelectedEnv()
|
||||||
|
|||||||
@@ -7,17 +7,14 @@ import {
|
|||||||
StaticSpotlightSearcherService,
|
StaticSpotlightSearcherService,
|
||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import IconLinkedIn from "~icons/brands/linkedin"
|
|
||||||
import IconTwitter from "~icons/brands/twitter"
|
|
||||||
import IconBook from "~icons/lucide/book"
|
import IconBook from "~icons/lucide/book"
|
||||||
import IconDiscord from "~icons/lucide/link"
|
import IconGithub from "~icons/lucide/github"
|
||||||
import IconGitHub from "~icons/lucide/github"
|
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||||
import IconZap from "~icons/lucide/zap"
|
import IconZap from "~icons/lucide/zap"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string | string[]
|
text: string
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
}
|
}
|
||||||
@@ -59,25 +56,10 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
alternates: ["key", "shortcuts", "binding"],
|
alternates: ["key", "shortcuts", "binding"],
|
||||||
icon: markRaw(IconZap),
|
icon: markRaw(IconZap),
|
||||||
},
|
},
|
||||||
link_github: {
|
social_links: {
|
||||||
text: [this.t("spotlight.general.social"), "GitHub"],
|
text: this.t("spotlight.general.social"),
|
||||||
alternates: ["social", "github", "link"],
|
alternates: ["social", "github", "binding"],
|
||||||
icon: markRaw(IconGitHub),
|
icon: markRaw(IconGithub),
|
||||||
},
|
|
||||||
link_twitter: {
|
|
||||||
text: [this.t("spotlight.general.social"), "Twitter"],
|
|
||||||
alternates: ["social", "twitter", "link"],
|
|
||||||
icon: markRaw(IconTwitter),
|
|
||||||
},
|
|
||||||
link_discord: {
|
|
||||||
text: [this.t("spotlight.general.social"), "Discord"],
|
|
||||||
alternates: ["social", "discord", "link"],
|
|
||||||
icon: markRaw(IconDiscord),
|
|
||||||
},
|
|
||||||
link_linkedin: {
|
|
||||||
text: [this.t("spotlight.general.social"), "LinkedIn"],
|
|
||||||
alternates: ["social", "linkedin", "link"],
|
|
||||||
icon: markRaw(IconLinkedIn),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,7 +87,8 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private openURL(url: string) {
|
private openDocs() {
|
||||||
|
const url = "https://docs.hoppscotch.io"
|
||||||
window.open(url, "_blank")
|
window.open(url, "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,22 +101,13 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
invokeAction("flyouts.chat.open")
|
invokeAction("flyouts.chat.open")
|
||||||
break
|
break
|
||||||
case "open_docs":
|
case "open_docs":
|
||||||
this.openURL("https://docs.hoppscotch.io")
|
this.openDocs()
|
||||||
break
|
break
|
||||||
case "open_keybindings":
|
case "open_keybindings":
|
||||||
invokeAction("flyouts.keybinds.toggle")
|
invokeAction("flyouts.keybinds.toggle")
|
||||||
break
|
break
|
||||||
case "link_github":
|
case "social_links":
|
||||||
this.openURL("https://hoppscotch.io/github")
|
invokeAction("modals.social.toggle")
|
||||||
break
|
|
||||||
case "link_twitter":
|
|
||||||
this.openURL("https://twitter.com/hoppscotch_io")
|
|
||||||
break
|
|
||||||
case "link_discord":
|
|
||||||
this.openURL("https://hoppscotch.io/discord")
|
|
||||||
break
|
|
||||||
case "link_linkedin":
|
|
||||||
this.openURL("https://www.linkedin.com/company/hoppscotch/")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { Ref, computed, effectScope, markRaw, ref, unref, watch } from "vue"
|
|
||||||
import { getI18n } from "~/modules/i18n"
|
|
||||||
import {
|
|
||||||
SpotlightSearcher,
|
|
||||||
SpotlightSearcherResult,
|
|
||||||
SpotlightSearcherSessionState,
|
|
||||||
SpotlightService,
|
|
||||||
} from ".."
|
|
||||||
|
|
||||||
import { Service } from "dioc"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import MiniSearch from "minisearch"
|
|
||||||
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
|
||||||
import IconCircle from "~icons/lucide/circle"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This searcher is responsible for searching through the interceptor.
|
|
||||||
* And switching between them.
|
|
||||||
*/
|
|
||||||
export class InterceptorSpotlightSearcherService
|
|
||||||
extends Service
|
|
||||||
implements SpotlightSearcher
|
|
||||||
{
|
|
||||||
public static readonly ID = "INTERCEPTOR_SPOTLIGHT_SEARCHER_SERVICE"
|
|
||||||
|
|
||||||
private t = getI18n()
|
|
||||||
|
|
||||||
public searcherID = "interceptor"
|
|
||||||
public searcherSectionTitle = this.t("settings.interceptor")
|
|
||||||
|
|
||||||
private readonly spotlight = this.bind(SpotlightService)
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.spotlight.registerSearcher(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private interceptorService = useService(InterceptorService)
|
|
||||||
|
|
||||||
createSearchSession(
|
|
||||||
query: Readonly<Ref<string>>
|
|
||||||
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
|
||||||
const loading = ref(false)
|
|
||||||
const results = ref<SpotlightSearcherResult[]>([])
|
|
||||||
|
|
||||||
const minisearch = new MiniSearch({
|
|
||||||
fields: ["name", "alternates"],
|
|
||||||
storeFields: ["name"],
|
|
||||||
})
|
|
||||||
|
|
||||||
const interceptorSelection = this.interceptorService
|
|
||||||
.currentInterceptorID as Ref<string>
|
|
||||||
|
|
||||||
const interceptors = this.interceptorService.availableInterceptors
|
|
||||||
|
|
||||||
minisearch.addAll(
|
|
||||||
interceptors.value.map((entry) => {
|
|
||||||
let id = `interceptor-${entry.interceptorID}`
|
|
||||||
if (entry.interceptorID === interceptorSelection.value) {
|
|
||||||
id += "-selected"
|
|
||||||
}
|
|
||||||
const name = unref(entry.name(this.t))
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
alternates: ["interceptor", "change", name],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const scopeHandle = effectScope()
|
|
||||||
|
|
||||||
scopeHandle.run(() => {
|
|
||||||
watch(
|
|
||||||
[query],
|
|
||||||
([query]) => {
|
|
||||||
results.value = minisearch
|
|
||||||
.search(query, {
|
|
||||||
prefix: true,
|
|
||||||
fuzzy: true,
|
|
||||||
boost: {
|
|
||||||
reltime: 2,
|
|
||||||
},
|
|
||||||
weights: {
|
|
||||||
fuzzy: 0.2,
|
|
||||||
prefix: 0.8,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.map((x) => {
|
|
||||||
return {
|
|
||||||
id: x.id,
|
|
||||||
icon: markRaw(
|
|
||||||
x.id.endsWith("-selected") ? IconCheckCircle : IconCircle
|
|
||||||
),
|
|
||||||
score: x.score,
|
|
||||||
text: {
|
|
||||||
type: "text",
|
|
||||||
text: [this.t("spotlight.section.interceptor"), x.name],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSessionEnd = () => {
|
|
||||||
scopeHandle.stop()
|
|
||||||
minisearch.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultObj = computed<SpotlightSearcherSessionState>(() => ({
|
|
||||||
loading: loading.value,
|
|
||||||
results: results.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return [resultObj, onSessionEnd]
|
|
||||||
}
|
|
||||||
|
|
||||||
onResultSelect(result: SpotlightSearcherResult): void {
|
|
||||||
const selectedInterceptor = result.id.split("-")[1]
|
|
||||||
this.interceptorService.currentInterceptorID.value = selectedInterceptor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, computed, markRaw, reactive } from "vue"
|
import { Component, markRaw, reactive } from "vue"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { SpotlightSearcherResult, SpotlightService } from ".."
|
import { SpotlightSearcherResult, SpotlightService } from ".."
|
||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
StaticSpotlightSearcherService,
|
StaticSpotlightSearcherService,
|
||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import { useRoute } from "vue-router"
|
|
||||||
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import IconWindow from "~icons/lucide/app-window"
|
import IconWindow from "~icons/lucide/app-window"
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
import IconCheck from "~icons/lucide/check"
|
||||||
|
import IconChevronLeft from "~icons/lucide/chevron-left"
|
||||||
|
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import IconCode2 from "~icons/lucide/code-2"
|
import IconCode2 from "~icons/lucide/code-2"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import IconFileCode from "~icons/lucide/file-code"
|
import IconFileCode from "~icons/lucide/file-code"
|
||||||
@@ -24,7 +25,6 @@ type Doc = {
|
|||||||
text: string | string[]
|
text: string | string[]
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
excludeFromSearch?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,160 +43,116 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
|
|
||||||
private readonly spotlight = this.bind(SpotlightService)
|
private readonly spotlight = this.bind(SpotlightService)
|
||||||
|
|
||||||
private route = useRoute()
|
|
||||||
private isRESTPage = computed(() => this.route.name === "index")
|
|
||||||
private isGQLPage = computed(() => this.route.name === "graphql")
|
|
||||||
|
|
||||||
private documents: Record<string, Doc> = reactive({
|
private documents: Record<string, Doc> = reactive({
|
||||||
send_request: {
|
send_request: {
|
||||||
text: this.t("shortcut.request.send_request"),
|
text: this.t("shortcut.request.send_request"),
|
||||||
alternates: ["request", "send"],
|
alternates: ["request", "send"],
|
||||||
icon: markRaw(IconPlay),
|
icon: markRaw(IconPlay),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
save_to_collections: {
|
save_to_collections: {
|
||||||
text: this.t("spotlight.request.save_as_new"),
|
text: [
|
||||||
|
this.t("request.save_as"),
|
||||||
|
this.t("shortcut.request.save_to_collections"),
|
||||||
|
],
|
||||||
alternates: ["save", "collections"],
|
alternates: ["save", "collections"],
|
||||||
icon: markRaw(IconSave),
|
icon: markRaw(IconSave),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
save_request: {
|
save_request: {
|
||||||
text: this.t("shortcut.request.save_request"),
|
text: this.t("shortcut.request.save_request"),
|
||||||
alternates: ["save", "request"],
|
alternates: ["save", "request"],
|
||||||
icon: markRaw(IconSave),
|
icon: markRaw(IconSave),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
rename_request: {
|
rename_request: {
|
||||||
text: this.t("shortcut.request.rename"),
|
text: this.t("shortcut.request.rename"),
|
||||||
alternates: ["rename", "request"],
|
alternates: ["rename", "request"],
|
||||||
icon: markRaw(IconRename),
|
icon: markRaw(IconRename),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
copy_request_link: {
|
copy_request_link: {
|
||||||
text: this.t("shortcut.request.copy_request_link"),
|
text: this.t("shortcut.request.copy_request_link"),
|
||||||
alternates: ["copy", "link"],
|
alternates: ["copy", "link"],
|
||||||
icon: markRaw(IconCopy),
|
icon: markRaw(IconCopy),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
reset_request: {
|
reset_request: {
|
||||||
text: this.t("shortcut.request.reset_request"),
|
text: this.t("shortcut.request.reset_request"),
|
||||||
alternates: ["reset", "request"],
|
alternates: ["reset", "request"],
|
||||||
icon: markRaw(IconRotateCCW),
|
icon: markRaw(IconRotateCCW),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
import_curl: {
|
import_curl: {
|
||||||
text: this.t("shortcut.request.import_curl"),
|
text: this.t("shortcut.request.import_curl"),
|
||||||
alternates: ["import", "curl"],
|
alternates: ["import", "curl"],
|
||||||
icon: markRaw(IconFileCode),
|
icon: markRaw(IconFileCode),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
show_code: {
|
show_code: {
|
||||||
text: this.t("shortcut.request.show_code"),
|
text: this.t("shortcut.request.show_code"),
|
||||||
alternates: ["show", "code"],
|
alternates: ["show", "code"],
|
||||||
icon: markRaw(IconCode2),
|
icon: markRaw(IconCode2),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
// Change request method
|
// Change request method
|
||||||
|
next_method: {
|
||||||
|
text: this.t("shortcut.request.next_method"),
|
||||||
|
alternates: ["next", "method"],
|
||||||
|
icon: markRaw(IconChevronRight),
|
||||||
|
},
|
||||||
|
previous_method: {
|
||||||
|
text: this.t("shortcut.request.previous_method"),
|
||||||
|
alternates: ["previous", "method"],
|
||||||
|
icon: markRaw(IconChevronLeft),
|
||||||
|
},
|
||||||
get_method: {
|
get_method: {
|
||||||
text: [this.t("spotlight.request.select_method"), "GET"],
|
text: this.t("shortcut.request.get_method"),
|
||||||
alternates: ["get", "method"],
|
alternates: ["get", "method"],
|
||||||
icon: markRaw(IconCheckCircle),
|
icon: markRaw(IconCheck),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
head_method: {
|
head_method: {
|
||||||
text: [this.t("spotlight.request.select_method"), "HEAD"],
|
text: this.t("shortcut.request.head_method"),
|
||||||
alternates: ["head", "method"],
|
alternates: ["head", "method"],
|
||||||
icon: markRaw(IconCheckCircle),
|
icon: markRaw(IconCheck),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
post_method: {
|
post_method: {
|
||||||
text: [this.t("spotlight.request.select_method"), "POST"],
|
text: this.t("shortcut.request.post_method"),
|
||||||
alternates: ["post", "method"],
|
alternates: ["post", "method"],
|
||||||
icon: markRaw(IconCheckCircle),
|
icon: markRaw(IconCheck),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
put_method: {
|
put_method: {
|
||||||
text: [this.t("spotlight.request.select_method"), "PUT"],
|
text: this.t("shortcut.request.put_method"),
|
||||||
alternates: ["put", "method"],
|
alternates: ["put", "method"],
|
||||||
icon: markRaw(IconCheckCircle),
|
icon: markRaw(IconCheck),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
delete_method: {
|
delete_method: {
|
||||||
text: [this.t("spotlight.request.select_method"), "DELETE"],
|
text: this.t("shortcut.request.delete_method"),
|
||||||
alternates: ["delete", "method"],
|
alternates: ["delete", "method"],
|
||||||
icon: markRaw(IconCheckCircle),
|
icon: markRaw(IconCheck),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
// Change sub tabs
|
// Change sub tabs
|
||||||
tab_parameters: {
|
tab_parameters: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_parameters"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_parameters"),
|
|
||||||
],
|
|
||||||
alternates: ["parameters", "tab"],
|
alternates: ["parameters", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_body: {
|
tab_body: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_body"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_body"),
|
|
||||||
],
|
|
||||||
alternates: ["body", "tab"],
|
alternates: ["body", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_headers: {
|
tab_headers: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_headers"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_headers"),
|
|
||||||
],
|
|
||||||
alternates: ["headers", "tab"],
|
alternates: ["headers", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_authorization: {
|
tab_authorization: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_authorization"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_authorization"),
|
|
||||||
],
|
|
||||||
alternates: ["authorization", "tab"],
|
alternates: ["authorization", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_pre_request_script: {
|
tab_pre_request_script: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_pre_request_script"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_pre_request_script"),
|
|
||||||
],
|
|
||||||
alternates: ["pre-request", "script", "tab"],
|
alternates: ["pre-request", "script", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
tab_tests: {
|
tab_tests: {
|
||||||
text: [
|
text: this.t("spotlight.request.tab_tests"),
|
||||||
this.t("spotlight.request.switch_to"),
|
|
||||||
this.t("spotlight.request.tab_tests"),
|
|
||||||
],
|
|
||||||
alternates: ["tests", "tab"],
|
alternates: ["tests", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -253,6 +209,12 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
case "reset_request":
|
case "reset_request":
|
||||||
invokeAction("request.reset")
|
invokeAction("request.reset")
|
||||||
break
|
break
|
||||||
|
case "next_method":
|
||||||
|
invokeAction("request.method.next")
|
||||||
|
break
|
||||||
|
case "previous_method":
|
||||||
|
invokeAction("request.method.prev")
|
||||||
|
break
|
||||||
case "get_method":
|
case "get_method":
|
||||||
invokeAction("request.method.get")
|
invokeAction("request.method.get")
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import {
|
|||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import IconCloud from "~icons/lucide/cloud"
|
import IconCloud from "~icons/lucide/cloud"
|
||||||
import IconGlobe from "~icons/lucide/globe"
|
|
||||||
import IconMonitor from "~icons/lucide/monitor"
|
import IconMonitor from "~icons/lucide/monitor"
|
||||||
import IconMoon from "~icons/lucide/moon"
|
import IconMoon from "~icons/lucide/moon"
|
||||||
import IconSun from "~icons/lucide/sun"
|
import IconSun from "~icons/lucide/sun"
|
||||||
|
import IconGlobe from "~icons/lucide/globe"
|
||||||
|
import IconShieldCheck from "~icons/lucide/shield-check"
|
||||||
import IconType from "~icons/lucide/type"
|
import IconType from "~icons/lucide/type"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
@@ -127,6 +128,22 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
alternates: ["language", "change language"],
|
alternates: ["language", "change language"],
|
||||||
icon: markRaw(IconGlobe),
|
icon: markRaw(IconGlobe),
|
||||||
},
|
},
|
||||||
|
change_interceptor: {
|
||||||
|
text: [
|
||||||
|
this.t("spotlight.section.interceptor"),
|
||||||
|
this.t("spotlight.settings.change_interceptor"),
|
||||||
|
],
|
||||||
|
alternates: ["interceptor", "change interceptor"],
|
||||||
|
icon: markRaw(IconShieldCheck),
|
||||||
|
},
|
||||||
|
install_ext: {
|
||||||
|
text: [
|
||||||
|
this.t("spotlight.section.interceptor"),
|
||||||
|
this.t("spotlight.settings.install_extension"),
|
||||||
|
],
|
||||||
|
alternates: ["install extension", "extension", "interceptor"],
|
||||||
|
icon: markRaw(IconShieldCheck),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -157,12 +174,27 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
applySetting("BG_COLOR", theme)
|
applySetting("BG_COLOR", theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
installExtension() {
|
||||||
|
const url = navigator.userAgent.includes("Firefox")
|
||||||
|
? "https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
|
: "https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
|
window.open(url, "_blank")
|
||||||
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
|
case "change_interceptor":
|
||||||
|
invokeAction("navigation.jump.settings")
|
||||||
|
break
|
||||||
|
|
||||||
case "change_lang":
|
case "change_lang":
|
||||||
invokeAction("navigation.jump.settings")
|
invokeAction("navigation.jump.settings")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case "install_ext":
|
||||||
|
this.installExtension()
|
||||||
|
break
|
||||||
|
|
||||||
// theme actions
|
// theme actions
|
||||||
case "theme_system":
|
case "theme_system":
|
||||||
invokeAction("settings.theme.system")
|
invokeAction("settings.theme.system")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, computed, markRaw, reactive } from "vue"
|
import { Component, markRaw, reactive } from "vue"
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { SpotlightSearcherResult, SpotlightService } from ".."
|
import { SpotlightSearcherResult, SpotlightService } from ".."
|
||||||
import {
|
import {
|
||||||
@@ -6,23 +6,19 @@ import {
|
|||||||
StaticSpotlightSearcherService,
|
StaticSpotlightSearcherService,
|
||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import { useRoute } from "vue-router"
|
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
|
||||||
import {
|
import {
|
||||||
closeOtherTabs,
|
closeOtherTabs,
|
||||||
closeTab,
|
closeTab,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
currentTabID,
|
currentTabID,
|
||||||
getActiveTabs,
|
|
||||||
} from "~/helpers/rest/tab"
|
} from "~/helpers/rest/tab"
|
||||||
import IconWindow from "~icons/lucide/app-window"
|
import IconWindow from "~icons/lucide/app-window"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string
|
text: string
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
excludeFromSearch?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,39 +37,21 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||||||
|
|
||||||
private readonly spotlight = this.bind(SpotlightService)
|
private readonly spotlight = this.bind(SpotlightService)
|
||||||
|
|
||||||
private route = useRoute()
|
|
||||||
private showAction = computed(
|
|
||||||
() => this.route.name === "index" ?? this.route.name === "graphql"
|
|
||||||
)
|
|
||||||
|
|
||||||
private documents: Record<string, Doc> = reactive({
|
private documents: Record<string, Doc> = reactive({
|
||||||
duplicate_tab: {
|
|
||||||
text: this.t("spotlight.tab.duplicate"),
|
|
||||||
alternates: ["tab", "duplicate", "duplicate tab"],
|
|
||||||
icon: markRaw(IconWindow),
|
|
||||||
excludeFromSearch: computed(() => !this.showAction.value),
|
|
||||||
},
|
|
||||||
close_current_tab: {
|
close_current_tab: {
|
||||||
text: this.t("spotlight.tab.close_current"),
|
text: this.t("spotlight.tab.close_current"),
|
||||||
alternates: ["tab", "close", "close tab"],
|
alternates: ["tab", "close", "close tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.showAction.value ?? getActiveTabs().value.length === 1
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
close_other_tabs: {
|
close_others_tab: {
|
||||||
text: this.t("spotlight.tab.close_others"),
|
text: this.t("spotlight.tab.close_others"),
|
||||||
alternates: ["tab", "close", "close all"],
|
alternates: ["tab", "close", "close all"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
|
||||||
() => !this.showAction.value ?? getActiveTabs().value.length < 2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
open_new_tab: {
|
open_new_tab: {
|
||||||
text: this.t("spotlight.tab.new_tab"),
|
text: this.t("spotlight.tab.new_tab"),
|
||||||
alternates: ["tab", "new", "open tab"],
|
alternates: ["tab", "new", "open tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(() => !this.showAction.value),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -102,12 +80,8 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
if (id === "duplicate_tab")
|
|
||||||
invokeAction("request.duplicate-tab", {
|
|
||||||
tabID: currentTabID.value,
|
|
||||||
})
|
|
||||||
if (id === "close_current_tab") closeTab(currentTabID.value)
|
if (id === "close_current_tab") closeTab(currentTabID.value)
|
||||||
if (id === "close_other_tabs") closeOtherTabs(currentTabID.value)
|
if (id === "close_others_tab") closeOtherTabs(currentTabID.value)
|
||||||
if (id === "open_new_tab")
|
if (id === "open_new_tab")
|
||||||
createNewTab({
|
createNewTab({
|
||||||
request: getDefaultRESTRequest(),
|
request: getDefaultRESTRequest(),
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import MiniSearch from "minisearch"
|
import MiniSearch from "minisearch"
|
||||||
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
|
||||||
import { useStreamStatic } from "~/composables/stream"
|
import { useStreamStatic } from "~/composables/stream"
|
||||||
import { runGQLQuery } from "~/helpers/backend/GQLClient"
|
import { runGQLQuery } from "~/helpers/backend/GQLClient"
|
||||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
@@ -37,7 +36,7 @@ import IconUserPlus from "~icons/lucide/user-plus"
|
|||||||
import IconUsers from "~icons/lucide/users"
|
import IconUsers from "~icons/lucide/users"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string | string[]
|
text: string
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
excludeFromSearch?: boolean
|
excludeFromSearch?: boolean
|
||||||
@@ -75,33 +74,30 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
|
|||||||
|
|
||||||
private documents: Record<string, Doc> = reactive({
|
private documents: Record<string, Doc> = reactive({
|
||||||
new_team: {
|
new_team: {
|
||||||
text: [this.t("team.title"), this.t("spotlight.workspace.new")],
|
text: this.t("spotlight.workspace.new"),
|
||||||
alternates: ["new", "team", "workspace"],
|
alternates: ["new", "team", "workspace"],
|
||||||
icon: markRaw(IconUsers),
|
icon: markRaw(IconUsers),
|
||||||
},
|
},
|
||||||
edit_team: {
|
edit_team: {
|
||||||
text: [this.t("team.title"), this.t("spotlight.workspace.edit")],
|
text: this.t("spotlight.workspace.edit"),
|
||||||
alternates: ["edit", "team", "workspace"],
|
alternates: ["edit", "team", "workspace"],
|
||||||
icon: markRaw(IconEdit),
|
icon: markRaw(IconEdit),
|
||||||
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
||||||
},
|
},
|
||||||
invite_members: {
|
invite_members: {
|
||||||
text: [this.t("team.title"), this.t("spotlight.workspace.invite")],
|
text: this.t("spotlight.workspace.invite"),
|
||||||
alternates: ["invite", "members", "workspace"],
|
alternates: ["invite", "members", "workspace"],
|
||||||
icon: markRaw(IconUserPlus),
|
icon: markRaw(IconUserPlus),
|
||||||
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
||||||
},
|
},
|
||||||
delete_team: {
|
delete_team: {
|
||||||
text: [this.t("team.title"), this.t("spotlight.workspace.delete")],
|
text: this.t("spotlight.workspace.delete"),
|
||||||
alternates: ["delete", "team", "workspace"],
|
alternates: ["delete", "team", "workspace"],
|
||||||
icon: markRaw(IconTrash2),
|
icon: markRaw(IconTrash2),
|
||||||
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
||||||
},
|
},
|
||||||
switch_to_personal: {
|
switch_to_personal: {
|
||||||
text: [
|
text: this.t("spotlight.workspace.switch_to_personal"),
|
||||||
this.t("team.title"),
|
|
||||||
this.t("spotlight.workspace.switch_to_personal"),
|
|
||||||
],
|
|
||||||
alternates: ["switch", "team", "workspace", "personal"],
|
alternates: ["switch", "team", "workspace", "personal"],
|
||||||
icon: markRaw(IconUser),
|
icon: markRaw(IconUser),
|
||||||
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
excludeFromSearch: computed(() => !this.isTeamSelected.value),
|
||||||
@@ -140,13 +136,8 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
if (id === "new_team") {
|
if (id === "new_team") invokeAction(`modals.team.new`)
|
||||||
if (platform.auth.getCurrentUser()) {
|
else if (id === "edit_team") invokeAction(`modals.team.edit`)
|
||||||
invokeAction(`modals.team.new`)
|
|
||||||
} else {
|
|
||||||
invokeAction(`modals.login.toggle`)
|
|
||||||
}
|
|
||||||
} else if (id === "edit_team") invokeAction(`modals.team.edit`)
|
|
||||||
else if (id === "invite_members") invokeAction(`modals.team.invite`)
|
else if (id === "invite_members") invokeAction(`modals.team.invite`)
|
||||||
else if (id === "delete_team") this.deleteTeam()
|
else if (id === "delete_team") this.deleteTeam()
|
||||||
else if (id === "switch_to_personal")
|
else if (id === "switch_to_personal")
|
||||||
@@ -197,14 +188,6 @@ export class SwitchWorkspaceSpotlightSearcherService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private workspace = useStreamStatic(
|
|
||||||
workspaceStatus$,
|
|
||||||
{ type: "personal" },
|
|
||||||
() => {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
createSearchSession(
|
createSearchSession(
|
||||||
query: Readonly<Ref<string>>
|
query: Readonly<Ref<string>>
|
||||||
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
||||||
@@ -219,16 +202,8 @@ export class SwitchWorkspaceSpotlightSearcherService
|
|||||||
this.fetchMyTeams().then((teams) => {
|
this.fetchMyTeams().then((teams) => {
|
||||||
minisearch.addAll(
|
minisearch.addAll(
|
||||||
teams.map((entry) => {
|
teams.map((entry) => {
|
||||||
let id = `workspace-${entry.id}`
|
|
||||||
// if id matches add -selected to it
|
|
||||||
if (
|
|
||||||
this.workspace.value.type === "team" &&
|
|
||||||
this.workspace.value.teamID === entry.id
|
|
||||||
) {
|
|
||||||
id += "-selected"
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: `workspace-${entry.id}`,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
alternates: ["team", "workspace", "change", "switch"],
|
alternates: ["team", "workspace", "change", "switch"],
|
||||||
}
|
}
|
||||||
@@ -257,9 +232,7 @@ export class SwitchWorkspaceSpotlightSearcherService
|
|||||||
.map((x) => {
|
.map((x) => {
|
||||||
return {
|
return {
|
||||||
id: x.id,
|
id: x.id,
|
||||||
icon: markRaw(
|
icon: markRaw(IconUsers),
|
||||||
x.id.endsWith("-selected") ? IconCheckCircle : IconUsers
|
|
||||||
),
|
|
||||||
score: x.score,
|
score: x.score,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export default defineConfig({
|
|||||||
"@lib": path.resolve(__dirname, "./src/lib"),
|
"@lib": path.resolve(__dirname, "./src/lib"),
|
||||||
stream: "stream-browserify",
|
stream: "stream-browserify",
|
||||||
util: "util",
|
util: "util",
|
||||||
querystring: "qs",
|
|
||||||
},
|
},
|
||||||
dedupe: ["vue"],
|
dedupe: ["vue"],
|
||||||
},
|
},
|
||||||
@@ -243,11 +242,9 @@ export default defineConfig({
|
|||||||
modernPolyfills: ["es.string.replace-all"],
|
modernPolyfills: ["es.string.replace-all"],
|
||||||
renderLegacyChunks: false,
|
renderLegacyChunks: false,
|
||||||
}),
|
}),
|
||||||
process.env.HOPP_ALLOW_RUNTIME_ENV
|
ImportMetaEnv.vite({
|
||||||
? ImportMetaEnv.vite({
|
example: "../../.env.example",
|
||||||
example: "../../.env.example",
|
env: "../../.env",
|
||||||
env: "../../.env",
|
}),
|
||||||
})
|
|
||||||
: [],
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-icon {
|
||||||
|
@apply transition;
|
||||||
|
@apply inline-flex;
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -326,7 +332,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e5cf"];
|
@apply after:content-["\e313"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,10 +487,6 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
|
||||||
@apply overscroll-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -572,11 +574,3 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gql-operation-not-highlight {
|
|
||||||
@apply opacity-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gql-operation-highlight {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export default defineConfig({
|
|||||||
process.env.HOPP_ALLOW_RUNTIME_ENV
|
process.env.HOPP_ALLOW_RUNTIME_ENV
|
||||||
? "VITE_BUILDTIME_"
|
? "VITE_BUILDTIME_"
|
||||||
: "VITE_",
|
: "VITE_",
|
||||||
envDir: path.resolve(__dirname, "../.."),
|
|
||||||
server: {
|
server: {
|
||||||
port: 3100,
|
port: 3100,
|
||||||
},
|
},
|
||||||
@@ -92,11 +91,9 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
process.env.HOPP_ALLOW_RUNTIME_ENV
|
ImportMetaEnv.vite({
|
||||||
? ImportMetaEnv.vite({
|
example: "../../.env.example",
|
||||||
example: "../../.env.example",
|
env: "../../.env",
|
||||||
env: "../../.env",
|
}),
|
||||||
})
|
|
||||||
: [],
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"do-build-ui": "pnpm run story:build"
|
"do-build-ui": "pnpm run story:build"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25",
|
||||||
|
"vue-router": "^4.0.16"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.0.5",
|
"@fontsource-variable/inter": "^5.0.5",
|
||||||
@@ -24,10 +25,25 @@
|
|||||||
"@lezer/highlight": "^1.0.0",
|
"@lezer/highlight": "^1.0.0",
|
||||||
"@vitejs/plugin-legacy": "^2.3.0",
|
"@vitejs/plugin-legacy": "^2.3.0",
|
||||||
"@vueuse/core": "^8.7.5",
|
"@vueuse/core": "^8.7.5",
|
||||||
|
"@vueuse/head": "^0.7.9",
|
||||||
|
"acorn-walk": "^8.2.0",
|
||||||
|
"esprima": "^4.0.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.12.1",
|
"fp-ts": "^2.12.1",
|
||||||
|
"globalthis": "^1.0.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
|
"rxjs": "^7.5.5",
|
||||||
|
"splitpanes": "^3.1.1",
|
||||||
|
"tern": "^0.24.3",
|
||||||
|
"timers": "^0.1.1",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"util": "^0.12.4",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vue-github-button": "^3.0.3",
|
||||||
|
"vue-router": "^4.0.16",
|
||||||
|
"vue-tippy": "6.0.0-alpha.58",
|
||||||
"vuedraggable-es": "^4.1.1"
|
"vuedraggable-es": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -45,10 +61,12 @@
|
|||||||
"@vue/compiler-sfc": "^3.2.39",
|
"@vue/compiler-sfc": "^3.2.39",
|
||||||
"@vue/eslint-config-typescript": "^11.0.1",
|
"@vue/eslint-config-typescript": "^11.0.1",
|
||||||
"@vue/runtime-core": "^3.2.39",
|
"@vue/runtime-core": "^3.2.39",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.5.1",
|
"eslint-plugin-vue": "^9.5.1",
|
||||||
"histoire": "^0.12.4",
|
"histoire": "^0.12.4",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
"rollup-plugin-polyfill-node": "^0.10.1",
|
"rollup-plugin-polyfill-node": "^0.10.1",
|
||||||
"sass": "^1.53.0",
|
"sass": "^1.53.0",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^4.5.4",
|
||||||
@@ -67,6 +85,8 @@
|
|||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
"vite-plugin-windicss": "^1.8.8",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
|
"vue-loader": "^16.8.3",
|
||||||
|
"vue-router": "^4.0.16",
|
||||||
"vue-tsc": "^0.38.2",
|
"vue-tsc": "^0.38.2",
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-icon {
|
||||||
|
@apply transition;
|
||||||
|
@apply inline-flex;
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -326,7 +332,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e5cf"];
|
@apply after:content-["\e313"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,10 +487,6 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
|
||||||
@apply overscroll-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -572,11 +574,3 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gql-operation-not-highlight {
|
|
||||||
@apply opacity-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gql-operation-highlight {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const emit = defineEmits<{
|
|||||||
@apply font-icon;
|
@apply font-icon;
|
||||||
@apply mr-2;
|
@apply mr-2;
|
||||||
@apply transition;
|
@apply transition;
|
||||||
@apply content-["\e5ca"];
|
@apply content-["\e876"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
<input
|
<input
|
||||||
:id="inputID"
|
:id="inputID"
|
||||||
class="input"
|
class="input"
|
||||||
ref="inputRef"
|
|
||||||
:class="inputStyles"
|
:class="inputStyles"
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
|
v-focus
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:type="type"
|
:type="type"
|
||||||
|
@keyup.enter="emit('submit')"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@@ -30,21 +31,12 @@ let inputIDCounter = 564275
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onKeyStroke, useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { defineProps, onMounted, ref, nextTick } from "vue"
|
import { defineProps } from "vue"
|
||||||
|
|
||||||
// Unique ID for input
|
// Unique ID for input
|
||||||
const inputID = `input-${inputIDCounter++}`
|
const inputID = `input-${inputIDCounter++}`
|
||||||
|
|
||||||
const inputRef = ref()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (props.autofocus) {
|
|
||||||
await nextTick()
|
|
||||||
inputRef.value?.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
id: string
|
id: string
|
||||||
@@ -55,7 +47,6 @@ const props = withDefaults(
|
|||||||
type: string
|
type: string
|
||||||
label: string
|
label: string
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
autofocus: boolean
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: "",
|
id: "",
|
||||||
@@ -66,7 +57,6 @@ const props = withDefaults(
|
|||||||
type: "text",
|
type: "text",
|
||||||
label: "",
|
label: "",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
autofocus: true,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,14 +66,4 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const inputText = useVModel(props, "modelValue", emit)
|
const inputText = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
onKeyStroke(
|
|
||||||
"Enter",
|
|
||||||
(e) => {
|
|
||||||
if (!e.repeat) {
|
|
||||||
return emit("submit")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ target: inputRef, eventName: "keydown" }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
|
<kbd class="mr-2 shortcut-key">ESC</kbd>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="dimissible"
|
v-if="dimissible"
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||||
@@ -108,22 +109,21 @@ const { t, onModalOpen, onModalClose } =
|
|||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
dialog: boolean
|
dialog: boolean,
|
||||||
title: string
|
title: string,
|
||||||
dimissible: boolean
|
dimissible: boolean,
|
||||||
placement: string
|
placement: string,
|
||||||
fullWidth: boolean
|
fullWidth: boolean,
|
||||||
styles: string
|
styles: string,
|
||||||
closeText: string | null
|
closeText: string | null,
|
||||||
}>(),
|
}>(), {
|
||||||
{
|
|
||||||
dialog: false,
|
dialog: false,
|
||||||
title: "",
|
title: "",
|
||||||
dimissible: true,
|
dimissible: true,
|
||||||
placement: "top",
|
placement: "top",
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
styles: "sm:max-w-lg",
|
styles: "sm:max-w-lg",
|
||||||
closeText: null,
|
closeText: null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
v-if="src"
|
v-if="src"
|
||||||
:src="src"
|
:src="src"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center mb-4"
|
class="inline-flex flex-col object-contain object-center my-4"
|
||||||
:class="large ? 'w-32 h-32' : 'w-16 h-16'"
|
:class="large ? 'w-32 h-32' : 'w-16 h-16'"
|
||||||
:alt="alt"
|
:alt="alt"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Transition name="fade" appear>
|
<Transition name="fade" appear>
|
||||||
<div
|
<div v-if="show" class="fixed inset-0 z-20 transition-opacity" @keydown.esc="close()">
|
||||||
v-if="show"
|
<div class="absolute inset-0 bg-primaryLight opacity-90 focus:outline-none" tabindex="0" @click="close()"></div>
|
||||||
class="fixed inset-0 z-20 transition-opacity"
|
|
||||||
@keydown.esc="close()"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-primaryLight opacity-90 focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@click="close()"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition name="slide" appear>
|
<Transition name="slide" appear>
|
||||||
<aside
|
<aside v-if="show"
|
||||||
v-if="show"
|
class="fixed top-0 right-0 z-30 flex flex-col h-full max-w-full overflow-auto border-l shadow-xl border-dividerDark bg-primary w-96">
|
||||||
class="fixed top-0 right-0 z-30 flex flex-col h-full max-w-full overflow-auto border-l shadow-xl border-dividerDark bg-primary w-96"
|
<div class="flex items-center justify-between p-2 border-b border-dividerLight">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between p-2 border-b border-dividerLight"
|
|
||||||
>
|
|
||||||
<h3 class="ml-4 heading">{{ title }}</h3>
|
<h3 class="ml-4 heading">{{ title }}</h3>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
|
<kbd class="mr-2 shortcut-key">ESC</kbd>
|
||||||
<HoppButtonSecondary :icon="IconX" @click="close()" />
|
<HoppButtonSecondary :icon="IconX" @click="close()" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,14 @@
|
|||||||
"@workers/*": ["./src/workers/*"],
|
"@workers/*": ["./src/workers/*"],
|
||||||
"@functional/*": ["./src/helpers/functional/*"]
|
"@functional/*": ["./src/helpers/functional/*"]
|
||||||
},
|
},
|
||||||
"types": ["vite/client", "unplugin-icons/types/vue"]
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"unplugin-icons/types/vue",
|
||||||
|
"unplugin-fonts/client",
|
||||||
|
"vite-plugin-pages/client",
|
||||||
|
"vite-plugin-vue-layouts/client",
|
||||||
|
"vite-plugin-pwa/client"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export default defineConfig({
|
|||||||
vue(),
|
vue(),
|
||||||
dts({
|
dts({
|
||||||
insertTypesEntry: true,
|
insertTypesEntry: true,
|
||||||
skipDiagnostics: true,
|
outDir: ["dist"],
|
||||||
outputDir: ["dist"],
|
|
||||||
}),
|
}),
|
||||||
WindiCSS({
|
WindiCSS({
|
||||||
root: path.resolve(__dirname),
|
root: path.resolve(__dirname),
|
||||||
@@ -49,7 +48,7 @@ export default defineConfig({
|
|||||||
fileName: (format, entry) => `${entry}.${format}.js`,
|
fileName: (format, entry) => `${entry}.${format}.js`,
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ["vue"],
|
external: ["vue", "vue-router"],
|
||||||
output: {
|
output: {
|
||||||
exports: "named",
|
exports: "named",
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user