Compare commits
45 Commits
revert/aut
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeee8af806 | ||
|
|
134441a6e7 | ||
|
|
80a5d21576 | ||
|
|
f1a812dae2 | ||
|
|
65a194a6d2 | ||
|
|
55e3dd3c18 | ||
|
|
b88f496f4e | ||
|
|
8caf9f110b | ||
|
|
1370b53726 | ||
|
|
8590a9a110 | ||
|
|
62058d5dfe | ||
|
|
1d397af674 | ||
|
|
141a468808 | ||
|
|
a24d724e2b | ||
|
|
dd72eacd21 | ||
|
|
c3c3fc6720 | ||
|
|
37a3b72025 | ||
|
|
defece95fc | ||
|
|
dbb45e7253 | ||
|
|
cc802b1e9f | ||
|
|
a66a2f5645 | ||
|
|
39afeab5f8 | ||
|
|
1583c86c78 | ||
|
|
1372681b87 | ||
|
|
2179ce6fff | ||
|
|
7e1b26c6a9 | ||
|
|
ae9b7183b5 | ||
|
|
3fa4052538 | ||
|
|
f2de0dc673 | ||
|
|
7e686a8882 | ||
|
|
4ca6e9ec3a | ||
|
|
dcd441f15e | ||
|
|
90c8fbeee4 | ||
|
|
cae1840506 | ||
|
|
82c6f6f6bc | ||
|
|
2545262fc2 | ||
|
|
b27fe871c4 | ||
|
|
cb5fff0310 | ||
|
|
d15caba4a6 | ||
|
|
536c8128dd | ||
|
|
99918ee0c0 | ||
|
|
864d40d934 | ||
|
|
a227af05d9 | ||
|
|
ce0898956d | ||
|
|
cd72851289 |
@@ -22,6 +22,10 @@ VITE_SHORTCODE_BASE_URL=https://hopp.sh
|
||||
VITE_BACKEND_GQL_URL=https://api.hoppscotch.io/graphql
|
||||
VITE_BACKEND_WS_URL=wss://api.hoppscotch.io/graphql
|
||||
|
||||
# Terms Of Service And Privacy Policy Links (Optional)
|
||||
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/terms
|
||||
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/privacy
|
||||
|
||||
# Sentry (Optional)
|
||||
# VITE_SENTRY_DSN: <Sentry DSN here>
|
||||
# VITE_SENTRY_ENVIRONMENT: <Sentry environment value here>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
# Packages
|
||||
/packages/codemirror-lang-graphql/ @AndrewBastin
|
||||
/packages/hoppscotch-cli/ @aitchnyu
|
||||
/packages/hoppscotch-cli/ @AndrewBastin
|
||||
/packages/hoppscotch-common/ @amk-dev @AndrewBastin
|
||||
/packages/hoppscotch-data/ @AndrewBastin
|
||||
/packages/hoppscotch-js-sandbox/ @aitchnyu
|
||||
/packages/hoppscotch-js-sandbox/ @AndrewBastin
|
||||
/packages/hoppscotch-ui/ @anwarulislam
|
||||
/packages/hoppscotch-web/ @amk-dev @AndrewBastin
|
||||
/packages/hoppscotch-web/ @amk-dev
|
||||
|
||||
# Sections within Hoppscotch Common
|
||||
/packages/hoppscotch-common/src/components @anwarulislam
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
# Hoppscotch CLI <sup>ALPHA</sup>
|
||||
<div align="center">
|
||||
<a href="https://hoppscotch.io">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/56705483"
|
||||
alt="Hoppscotch Logo"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
|
||||
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
||||
|
||||
</div>
|
||||
|
||||
A CLI to run Hoppscotch test scripts in CI environments.
|
||||
|
||||
@@ -33,7 +46,7 @@ hopp [options or commands] arguments
|
||||
|
||||
#### Options:
|
||||
##### `-e <file_path>` / `--env <file_path>`
|
||||
- Accepts path to env.json with contents in below format:
|
||||
- Accepts path to env.json with contents in below format:
|
||||
```json
|
||||
{
|
||||
"ENV1":"value1",
|
||||
@@ -41,7 +54,7 @@ hopp [options or commands] arguments
|
||||
}
|
||||
```
|
||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||
|
||||
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
|
||||
## Install
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
@@ -36,8 +36,8 @@
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^0.4.4",
|
||||
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@swc/core": "^1.2.181",
|
||||
"@types/axios": "^0.14.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"io-ts": "^2.2.16",
|
||||
"jest": "^27.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier": "^2.8.4",
|
||||
"qs": "^6.10.3",
|
||||
"ts-jest": "^27.1.4",
|
||||
"tsup": "^5.12.7",
|
||||
|
||||
@@ -5,42 +5,52 @@ import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const cmd = `node ./bin/hopp test`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const cmd = `node ./bin/hopp test notfound.json`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const cmd = `node ./bin/hopp invalid-arg`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
@@ -70,24 +80,24 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("ENV file not JSON type.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("ENV file not found.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
@@ -109,17 +119,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
|
||||
test("No value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Invalid value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
console.log("invalid value thing", out)
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { checkFile } from "../../../utils/checks";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("checkFile", () => {
|
||||
test("File doesn't exists.", () => {
|
||||
return expect(
|
||||
checkFile("./src/samples/this-file-not-exists.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
});
|
||||
});
|
||||
|
||||
test("File not of JSON type.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/notjson.txt")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "INVALID_FILE_TYPE",
|
||||
});
|
||||
});
|
||||
|
||||
test("Existing JSON file.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/passes.json")()
|
||||
).resolves.toBeRight();
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe("collectionsRunner", () => {
|
||||
|
||||
test("Empty HoppCollection.", () => {
|
||||
return expect(
|
||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
|
||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })
|
||||
).resolves.toStrictEqual([]);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([]);
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/request",
|
||||
@@ -116,7 +116,7 @@ describe("collectionsRunner", () => {
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
})
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/folder/request",
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { parseCollectionData } from "../../../utils/mutators";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("parseCollectionData", () => {
|
||||
test("Reading non-existing file.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/notexist.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
parseCollectionData("./src/__tests__/samples/notexist.json")
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
});
|
||||
});
|
||||
|
||||
test("Unparseable JSON contents.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/malformed-collection.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "MALFORMED_COLLECTION",
|
||||
parseCollectionData("./src/__tests__/samples/malformed-collection.json")
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "UNKNOWN_ERROR",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,15 +22,15 @@ describe("parseCollectionData", () => {
|
||||
return expect(
|
||||
parseCollectionData(
|
||||
"./src/__tests__/samples/malformed-collection2.json"
|
||||
)()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
)
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "MALFORMED_COLLECTION",
|
||||
});
|
||||
});
|
||||
|
||||
test("Valid HoppCollection.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/passes.json")()
|
||||
).resolves.toBeRight();
|
||||
parseCollectionData("./src/__tests__/samples/passes.json")
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { pipe, flow } from "fp-ts/function";
|
||||
import {
|
||||
collectionsRunner,
|
||||
collectionsRunnerExit,
|
||||
@@ -10,18 +8,23 @@ import { parseCollectionData } from "../utils/mutators";
|
||||
import { parseEnvsData } from "../options/test/env";
|
||||
import { TestCmdOptions } from "../types/commands";
|
||||
import { parseDelayOption } from "../options/test/delay";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { isHoppCLIError } from "../utils/checks";
|
||||
|
||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||
await pipe(
|
||||
TE.Do,
|
||||
TE.bind("envs", () => parseEnvsData(options.env)),
|
||||
TE.bind("collections", () => parseCollectionData(path)),
|
||||
TE.bind("delay", () => parseDelayOption(options.delay)),
|
||||
TE.chainTaskK(collectionsRunner),
|
||||
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
||||
TE.mapLeft((e) => {
|
||||
handleError(e);
|
||||
try {
|
||||
const delay = options.delay ? parseDelayOption(options.delay) : 0
|
||||
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
|
||||
const collections = await parseCollectionData(path)
|
||||
|
||||
const report = await collectionsRunner({collections, envs, delay})
|
||||
const hasSucceeded = collectionsRunnerResult(report)
|
||||
collectionsRunnerExit(hasSucceeded)
|
||||
} catch(e) {
|
||||
if(isHoppCLIError(e)) {
|
||||
handleError(e)
|
||||
process.exit(1);
|
||||
})
|
||||
)();
|
||||
}
|
||||
else throw e
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { log } from "console";
|
||||
import * as S from "fp-ts/string";
|
||||
import { HoppError, HoppErrorCode } from "../types/errors";
|
||||
import { hasProperty, isSafeCommanderError } from "../utils/checks";
|
||||
@@ -7,7 +6,7 @@ import { exceptionColors } from "../utils/getters";
|
||||
const { BG_FAIL } = exceptionColors;
|
||||
|
||||
/**
|
||||
* Parses unknown error data and narrows it to get information realted to
|
||||
* Parses unknown error data and narrows it to get information related to
|
||||
* error in string format.
|
||||
* @param e Error data to parse.
|
||||
* @returns Information in string format appropriately parsed, based on error type.
|
||||
@@ -81,6 +80,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
}
|
||||
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
log(ERROR_CODE, ERROR_MSG);
|
||||
console.error(ERROR_CODE, ERROR_MSG);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,20 +1,14 @@
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as S from "fp-ts/string";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { error, HoppCLIError } from "../../types/errors";
|
||||
import { error } from "../../types/errors";
|
||||
|
||||
export const parseDelayOption = (
|
||||
delay: unknown
|
||||
): TE.TaskEither<HoppCLIError, number> =>
|
||||
!S.isString(delay)
|
||||
? TE.right(0)
|
||||
: pipe(
|
||||
delay,
|
||||
Number,
|
||||
TE.fromPredicate(Number.isSafeInteger, () =>
|
||||
error({
|
||||
code: "INVALID_ARGUMENT",
|
||||
data: "Expected '-d, --delay' value to be number",
|
||||
})
|
||||
)
|
||||
);
|
||||
export function parseDelayOption(delay: string): number {
|
||||
const maybeInt = Number.parseInt(delay)
|
||||
|
||||
if(!Number.isNaN(maybeInt)) {
|
||||
return maybeInt
|
||||
} else {
|
||||
throw error({
|
||||
code: "INVALID_ARGUMENT",
|
||||
data: "Expected '-d, --delay' value to be number",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,27 @@
|
||||
import fs from "fs/promises";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as J from "fp-ts/Json";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import isArray from "lodash/isArray";
|
||||
import { HoppCLIError, error } from "../../types/errors";
|
||||
import { error } from "../../types/errors";
|
||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||
import { checkFile } from "../../utils/checks";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
export const parseEnvsData = (
|
||||
path: unknown
|
||||
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
|
||||
!S.isString(path)
|
||||
? TE.right({ global: [], selected: [] })
|
||||
: pipe(
|
||||
// Checking if the env.json file exists or not.
|
||||
checkFile(path),
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path)
|
||||
|
||||
// Trying to read given env json file path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) =>
|
||||
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
),
|
||||
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||
}
|
||||
|
||||
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
|
||||
TE.chainEitherKW((data) =>
|
||||
pipe(
|
||||
data.toString(),
|
||||
J.parse,
|
||||
E.map((jsonData) =>
|
||||
jsonData && typeof jsonData === "object" && !isArray(jsonData)
|
||||
? pipe(
|
||||
jsonData,
|
||||
Object.entries,
|
||||
A.map(
|
||||
([key, value]) =>
|
||||
<HoppEnvPair>{
|
||||
key,
|
||||
value: S.isString(value)
|
||||
? value
|
||||
: JSON.stringify(value),
|
||||
}
|
||||
)
|
||||
)
|
||||
: []
|
||||
),
|
||||
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
|
||||
E.mapLeft((e) =>
|
||||
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const envPairs: Array<HoppEnvPair> = []
|
||||
|
||||
for( const [key,value] of Object.entries(contents)) {
|
||||
if(typeof value !== "string") {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
||||
}
|
||||
|
||||
envPairs.push({key, value})
|
||||
}
|
||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type TestCmdOptions = {
|
||||
env: string;
|
||||
delay: number;
|
||||
env: string | undefined;
|
||||
delay: string | undefined;
|
||||
};
|
||||
|
||||
export type HoppEnvFileExt = "json";
|
||||
export type HOPP_ENV_FILE_EXT = "json";
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import fs from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
isHoppRESTRequest,
|
||||
} from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import curryRight from "lodash/curryRight";
|
||||
import { CommanderError } from "commander";
|
||||
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
import { HoppCollectionFileExt } from "../types/collections";
|
||||
import { HoppEnvFileExt } from "../types/commands";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
|
||||
/**
|
||||
* Determines whether an object has a property with given name.
|
||||
@@ -71,59 +62,6 @@ export const isRESTCollection = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the file path matches the requried file type with of required extension.
|
||||
* @param path The input file path to check.
|
||||
* @param extension The required extension for input file path.
|
||||
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFileExt = curryRight(
|
||||
(
|
||||
path: unknown,
|
||||
extension: HoppCollectionFileExt | HoppEnvFileExt
|
||||
): E.Either<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
|
||||
E.chainW(
|
||||
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
|
||||
error({ code: "INVALID_FILE_TYPE", data: extension })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if the given file path exists and is of given type.
|
||||
* @param path The input file path to check.
|
||||
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
|
||||
// Checking if path is string.
|
||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
||||
|
||||
/**
|
||||
* After checking file path, we map file path to absolute path and check
|
||||
* if file is of given extension type.
|
||||
*/
|
||||
TE.map(join),
|
||||
TE.chainEitherK(checkFileExt("json")),
|
||||
|
||||
/**
|
||||
* Trying to access given file path.
|
||||
* If successfully accessed, we return the path from predicate step.
|
||||
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
||||
*/
|
||||
TE.chainFirstW((checkedPath) =>
|
||||
TE.tryCatchK(
|
||||
() => fs.access(checkedPath),
|
||||
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
|
||||
)()
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if given error data is of type HoppCLIError, based on existence
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { bold } from "chalk";
|
||||
@@ -43,8 +42,8 @@ const { WARN, FAIL } = exceptionColors;
|
||||
* @returns List of report for each processed request.
|
||||
*/
|
||||
export const collectionsRunner =
|
||||
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
|
||||
async () => {
|
||||
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
|
||||
{
|
||||
const envs: HoppEnvs = param.envs;
|
||||
const delay = param.delay ?? 0;
|
||||
const requestsReport: RequestReport[] = [];
|
||||
@@ -213,10 +212,10 @@ export const collectionsRunnerResult = (
|
||||
* Else, exit with code 1.
|
||||
* @param result Boolean defining the collections-runner result.
|
||||
*/
|
||||
export const collectionsRunnerExit = (result: boolean) => {
|
||||
export const collectionsRunnerExit = (result: boolean): never => {
|
||||
if (!result) {
|
||||
const EXIT_MSG = FAIL(`\nExited with code 1`);
|
||||
process.stdout.write(EXIT_MSG);
|
||||
process.stderr.write(EXIT_MSG);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as J from "fp-ts/Json";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
|
||||
import { error } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
|
||||
/**
|
||||
@@ -39,49 +34,44 @@ export const parseErrorMessage = (e: unknown) => {
|
||||
return msg.replace(/\n+$|\s{2,}/g, "").trim();
|
||||
};
|
||||
|
||||
export async function readJsonFile(path: string): Promise<unknown> {
|
||||
if(!path.endsWith('.json')) {
|
||||
throw error({ code: "INVALID_FILE_TYPE", data: path })
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(path)
|
||||
} catch (e) {
|
||||
throw error({ code: "FILE_NOT_FOUND", path: path })
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse((await fs.readFile(path)).toString())
|
||||
} catch(e) {
|
||||
throw error({ code: "UNKNOWN_ERROR", data: e })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses collection json file for given path:context.path, and validates
|
||||
* the parsed collectiona array.
|
||||
* @param path Collection json file path.
|
||||
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||
*/
|
||||
export const parseCollectionData = (
|
||||
export async function parseCollectionData(
|
||||
path: string
|
||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
||||
pipe(
|
||||
TE.of(path),
|
||||
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||
let contents = await readJsonFile(path)
|
||||
|
||||
// Checking if given file path exists or not.
|
||||
TE.chain(checkFile),
|
||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
|
||||
|
||||
// Trying to read give collection json path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
),
|
||||
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
})
|
||||
}
|
||||
|
||||
// Checking if parsed file data is array.
|
||||
TE.chainEitherKW((data) =>
|
||||
pipe(
|
||||
data.toString(),
|
||||
J.parse,
|
||||
E.map((jsonData) => (Array.isArray(jsonData) ? jsonData : [jsonData])),
|
||||
E.mapLeft((e) =>
|
||||
error({ code: "MALFORMED_COLLECTION", path, data: E.toError(e) })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Validating collections to be HoppRESTCollection.
|
||||
TE.chainW(
|
||||
TE.fromPredicate(A.every(isRESTCollection), () =>
|
||||
error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 00-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0020 4.77 5.07 5.07 0 0019.91 1S18.73.65 16 2.48a13.38 13.38 0 00-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 005 4.77a5.44 5.44 0 00-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 009 18.13V22" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 504 B |
@@ -34,7 +34,7 @@ input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-35;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -323,9 +323,10 @@ pre.ace_editor {
|
||||
@apply after:justify-center;
|
||||
@apply after:pointer-events-none;
|
||||
@apply after:font-icon;
|
||||
@apply after:text-secondaryLight;
|
||||
@apply after:text-current;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e313"];
|
||||
@apply after:text-lg;
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@@ -416,7 +417,6 @@ pre.ace_editor {
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
@apply before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@@ -424,48 +424,39 @@ pre.ace_editor {
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-0;
|
||||
@apply after:z-20;
|
||||
@apply after:transition;
|
||||
@apply after:flex;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:text-dividerDark;
|
||||
@apply after:font-icon;
|
||||
@apply hover:before:opacity-100;
|
||||
@apply hover:after:text-accentDark;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-1;
|
||||
@apply w-0;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply after:content-["\e5d4"];
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-1;
|
||||
@apply h-0;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply after:content-["\e5d3"];
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0.5;
|
||||
@apply w-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0.5;
|
||||
@apply h-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
}
|
||||
|
||||
@mixin dark-theme {
|
||||
--primary-color: theme("colors.neutral.900");
|
||||
--primary-color: theme("colors.dark.800");
|
||||
--primary-light-color: theme("colors.dark.600");
|
||||
--primary-dark-color: theme("colors.neutral.800");
|
||||
--primary-contrast-color: #161616;
|
||||
--primary-contrast-color: theme("colors.neutral.900");
|
||||
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--secondary-dark-color: theme("colors.neutral.50");
|
||||
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
--divider-light-color: theme("colors.dark.500");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
|
||||
--error-color: theme("colors.stone.800");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.700");
|
||||
@@ -24,15 +27,18 @@
|
||||
|
||||
@mixin light-theme {
|
||||
--primary-color: theme("colors.white");
|
||||
--primary-light-color: theme("colors.neutral.50");
|
||||
--primary-dark-color: theme("colors.neutral.100");
|
||||
--primary-contrast-color: #fefefe;
|
||||
--secondary-color: theme("colors.neutral.500");
|
||||
--secondary-light-color: theme("colors.neutral.400");
|
||||
--secondary-dark-color: theme("colors.neutral.900");
|
||||
--primary-light-color: theme("colors.gray.50");
|
||||
--primary-dark-color: theme("colors.gray.100");
|
||||
--primary-contrast-color: theme("colors.light.50");
|
||||
|
||||
--secondary-color: theme("colors.gray.500");
|
||||
--secondary-light-color: theme("colors.gray.400");
|
||||
--secondary-dark-color: theme("colors.gray.900");
|
||||
|
||||
--divider-color: theme("colors.gray.100");
|
||||
--divider-light-color: theme("colors.neutral.100");
|
||||
--divider-dark-color: theme("colors.neutral.300");
|
||||
--divider-light-color: theme("colors.gray.100");
|
||||
--divider-dark-color: theme("colors.gray.300");
|
||||
|
||||
--error-color: theme("colors.yellow.100");
|
||||
--tooltip-color: theme("colors.neutral.800");
|
||||
--popover-color: theme("colors.white");
|
||||
@@ -43,16 +49,19 @@
|
||||
--primary-color: theme("colors.dark.900");
|
||||
--primary-light-color: theme("colors.neutral.900");
|
||||
--primary-dark-color: theme("colors.dark.800");
|
||||
--primary-contrast-color: #0e0e0e;
|
||||
--primary-contrast-color: theme("colors.dark.900");
|
||||
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
|
||||
--divider-color: theme("colors.dark.600");
|
||||
--divider-light-color: theme("colors.dark.800");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
--divider-dark-color: theme("colors.dark.200");
|
||||
|
||||
--error-color: theme("colors.stone.900");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.600");
|
||||
--popover-color: theme("colors.dark.900");
|
||||
--editor-theme: "twilight";
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"edit": "Edit",
|
||||
"filter": "Filter",
|
||||
"go_back": "Go back",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
@@ -117,12 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "Collection created",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Edit Collection",
|
||||
"invalid_name": "Please provide a name for the collection",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"my_collections": "My Collections",
|
||||
"name": "My New Collection",
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "New Collection",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"renamed": "Collection renamed",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Save as",
|
||||
@@ -133,6 +138,7 @@
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "Are you sure you want to leave this team?",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab ?",
|
||||
"logout": "Are you sure you want to logout?",
|
||||
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
||||
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
||||
@@ -312,6 +318,7 @@
|
||||
"modal": {
|
||||
"collections": "Collections",
|
||||
"confirm": "Confirm",
|
||||
"close_unsaved_tab": "Close Unsaved Tab ?",
|
||||
"edit_request": "Edit Request",
|
||||
"import_export": "Import / Export"
|
||||
},
|
||||
@@ -389,16 +396,19 @@
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Copy link",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Duration",
|
||||
"enter_curl": "Enter cURL command",
|
||||
"duplicated": "Request duplicated",
|
||||
"generate_code": "Generate code",
|
||||
"generated_code": "Generated code",
|
||||
"header_list": "Header List",
|
||||
"invalid_name": "Please provide a name for the request",
|
||||
"method": "Method",
|
||||
"moved": "Request moved",
|
||||
"name": "Request name",
|
||||
"new": "New Request",
|
||||
"order_changed": "Request Order Updated",
|
||||
"override": "Override",
|
||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
||||
"overriden": "Overridden",
|
||||
@@ -629,6 +639,7 @@
|
||||
"body": "Body",
|
||||
"collections": "Collections",
|
||||
"documentation": "Documentation",
|
||||
"environments": "Environments",
|
||||
"headers": "Headers",
|
||||
"history": "History",
|
||||
"mqtt": "MQTT",
|
||||
@@ -655,6 +666,7 @@
|
||||
"exit_disabled": "Only owner cannot exit the team",
|
||||
"invalid_email_format": "Email format is invalid",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_invite_link": "Invalid invite link",
|
||||
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
|
||||
"invalid_member_permission": "Please provide a valid permission to the team member",
|
||||
@@ -676,6 +688,7 @@
|
||||
"member_removed": "User removed",
|
||||
"member_role_updated": "User roles updated",
|
||||
"members": "Members",
|
||||
"more_members": "+{count} more",
|
||||
"name_length_insufficient": "Team name should be at least 6 characters long",
|
||||
"name_updated": "Team name updated",
|
||||
"new": "New Team",
|
||||
@@ -683,10 +696,13 @@
|
||||
"new_name": "My New Team",
|
||||
"no_access": "You do not have edit access to these collections",
|
||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
||||
"no_request_found": "Request not found.",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"pending_invites": "Pending invites",
|
||||
"permissions": "Permissions",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "Team saved",
|
||||
"select_a_team": "Select a team",
|
||||
"title": "Teams",
|
||||
@@ -714,5 +730,11 @@
|
||||
"message": "Message",
|
||||
"protocols": "Protocols",
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "3.0.1",
|
||||
"version": "2023.4.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"dev:vite": "vite",
|
||||
@@ -30,10 +30,10 @@
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.1.0",
|
||||
"@codemirror/view": "^6.0.2",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0",
|
||||
"@hoppscotch/ui": "workspace:^0.0.1",
|
||||
"@hoppscotch/data": "workspace:^0.4.4",
|
||||
"@hoppscotch/js-sandbox": "workspace:^2.1.0",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@sentry/tracing": "^7.13.0",
|
||||
@@ -50,7 +50,6 @@
|
||||
"buffer": "^6.0.3",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"firebase": "^9.8.4",
|
||||
"fp-ts": "^2.12.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"globalthis": "^1.0.3",
|
||||
@@ -90,7 +89,7 @@
|
||||
"vue-pdf-embed": "^1.1.4",
|
||||
"vue-router": "^4.0.16",
|
||||
"vue-tippy": "6.0.0-alpha.58",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^4.0.15",
|
||||
"workbox-window": "^6.5.4",
|
||||
"yargs-parser": "^21.1.1"
|
||||
@@ -125,8 +124,9 @@
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"@vue/runtime-core": "^3.2.39",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-prettier": "^4.2.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-if="isLoadingInitialRoute"
|
||||
class="flex flex-col items-center justify-center min-h-screen"
|
||||
>
|
||||
<SmartSpinner />
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<ErrorPage v-if="errorInfo !== null" :error="errorInfo" />
|
||||
<RouterView v-else />
|
||||
|
||||
52
packages/hoppscotch-common/src/components.d.ts
vendored
52
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -26,8 +26,6 @@ declare module '@vue/runtime-core' {
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||
Collections: typeof import('./components/collections/index.vue')['default']
|
||||
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
|
||||
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
|
||||
@@ -52,9 +50,7 @@ declare module '@vue/runtime-core' {
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
CollectionsTeamSelect: typeof import('./components/collections/TeamSelect.vue')['default']
|
||||
Environments: typeof import('./components/environments/index.vue')['default']
|
||||
EnvironmentsChooseType: typeof import('./components/environments/ChooseType.vue')['default']
|
||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||
@@ -75,7 +71,28 @@ declare module '@vue/runtime-core' {
|
||||
History: typeof import('./components/history/index.vue')['default']
|
||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||
HttpBody: typeof import('./components/http/Body.vue')['default']
|
||||
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
||||
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
||||
@@ -88,6 +105,7 @@ declare module '@vue/runtime-core' {
|
||||
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
|
||||
HttpRequest: typeof import('./components/http/Request.vue')['default']
|
||||
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
|
||||
HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
|
||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||
@@ -97,14 +115,15 @@ declare module '@vue/runtime-core' {
|
||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||
@@ -128,41 +147,24 @@ declare module '@vue/runtime-core' {
|
||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
|
||||
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
|
||||
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
|
||||
SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||
SmartTree: typeof import('./components/smart/Tree.vue')['default']
|
||||
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
|
||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||
Teams: typeof import('./components/teams/index.vue')['default']
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||
TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
||||
TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
|
||||
TeamsModal: typeof import('./components/teams/Modal.vue')['default']
|
||||
TeamsTeam: typeof import('./components/teams/Team.vue')['default']
|
||||
Tippy: typeof import('vue-tippy')['Tippy']
|
||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('app.developer_option')"
|
||||
@@ -10,7 +10,7 @@
|
||||
{{ t("app.developer_option_description") }}
|
||||
</p>
|
||||
<div class="flex flex-1">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
filled
|
||||
:icon="copyIcon"
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="flex justify-between bg-primary">
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="EXPAND_NAVIGATION ? t('hide.sidebar') : t('show.sidebar')"
|
||||
:icon="IconSidebar"
|
||||
@@ -10,7 +10,7 @@
|
||||
:class="{ '-rotate-180': !EXPAND_NAVIGATION }"
|
||||
@click="EXPAND_NAVIGATION = !EXPAND_NAVIGATION"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${ZEN_MODE ? t('action.turn_off') : t('action.turn_on')} ${t(
|
||||
'layout.zen_mode'
|
||||
@@ -23,7 +23,7 @@
|
||||
@click="ZEN_MODE = !ZEN_MODE"
|
||||
/>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('settings.interceptor')"
|
||||
:icon="IconShieldCheck"
|
||||
@@ -40,8 +40,8 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
:icon="IconHelpCircle"
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLifeBuoy"
|
||||
class="!rounded-none"
|
||||
:label="`${t('app.help')}`"
|
||||
/>
|
||||
@@ -55,7 +55,7 @@
|
||||
@keyup.c="chat!.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="documentation"
|
||||
:icon="IconBook"
|
||||
:label="`${t('app.documentation')}`"
|
||||
@@ -64,7 +64,7 @@
|
||||
:shortcut="['D']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="shortcuts"
|
||||
:icon="IconZap"
|
||||
:label="`${t('app.keyboard_shortcuts')}`"
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="chat"
|
||||
:icon="IconMessageCircle"
|
||||
:label="`${t('app.chat_with_us')}`"
|
||||
@@ -88,14 +88,14 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="`${t('app.whats_new')}`"
|
||||
to="https://docs.hoppscotch.io/changelog"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconActivity"
|
||||
:label="t('app.status')"
|
||||
to="https://status.hoppscotch.io"
|
||||
@@ -103,21 +103,21 @@
|
||||
@click="hide()"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="`${t('app.github')}`"
|
||||
to="https://github.com/hoppscotch/hoppscotch"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconTwitter"
|
||||
:label="`${t('app.twitter')}`"
|
||||
to="https://hoppscotch.io/twitter"
|
||||
blank
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconUserPlus"
|
||||
:label="`${t('app.invite')}`"
|
||||
@click="
|
||||
@@ -127,7 +127,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconLock"
|
||||
:label="`${t('app.terms_and_privacy')}`"
|
||||
to="https://docs.hoppscotch.io/privacy"
|
||||
@@ -148,7 +148,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'app.shortcuts'
|
||||
@@ -156,14 +156,14 @@
|
||||
:icon="IconZap"
|
||||
@click="invokeAction('flyouts.keybinds.toggle')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="navigatorShare"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconShare2"
|
||||
:title="t('request.share')"
|
||||
@click="nativeShare()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="COLUMN_LAYOUT ? t('layout.row') : t('layout.column')"
|
||||
:icon="IconColumns"
|
||||
@@ -177,7 +177,7 @@
|
||||
'rotate-180': SIDEBAR_ON_LEFT,
|
||||
}"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="SIDEBAR ? t('hide.sidebar') : t('show.sidebar')"
|
||||
:icon="IconSidebarOpen"
|
||||
@@ -206,7 +206,6 @@ import IconShare2 from "~icons/lucide/share-2"
|
||||
import IconColumns from "~icons/lucide/columns"
|
||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||
import IconShieldCheck from "~icons/lucide/shield-check"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconBook from "~icons/lucide/book"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
@@ -215,6 +214,7 @@ import IconGithub from "~icons/lucide/github"
|
||||
import IconTwitter from "~icons/lucide/twitter"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import { showChat } from "@modules/crisp"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
@@ -223,7 +223,7 @@ import { platform } from "~/platform"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
const showDeveloperOptions = ref(false)
|
||||
@@ -274,7 +274,7 @@ const showDeveloperOptionModal = () => {
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const documentation = ref<typeof SmartItem>()
|
||||
const shortcuts = ref<typeof SmartItem>()
|
||||
const chat = ref<typeof SmartItem>()
|
||||
const documentation = ref<typeof HoppSmartItem>()
|
||||
const shortcuts = ref<typeof HoppSmartItem>()
|
||||
const chat = ref<typeof HoppSmartItem>()
|
||||
</script>
|
||||
|
||||
@@ -4,21 +4,28 @@
|
||||
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center space-x-2"
|
||||
class="inline-flex items-center justify-start flex-1 space-x-2"
|
||||
:style="{
|
||||
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
||||
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
||||
}"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
||||
:label="t('app.name')"
|
||||
to="/"
|
||||
/>
|
||||
<AppGitHubStarButton class="mt-1.5 transition <sm:hidden" />
|
||||
<!-- <AppGitHubStarButton class="mt-1.5 transition" /> -->
|
||||
</div>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||
:icon="IconSearch"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="showInstallButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('header.install_pwa')"
|
||||
@@ -26,14 +33,7 @@
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="installPWA()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||
:icon="IconSearch"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${
|
||||
mdAndLarger ? t('support.title') : t('app.options')
|
||||
@@ -42,28 +42,81 @@
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.support.toggle')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<div
|
||||
v-if="currentUser === null"
|
||||
:icon="IconUploadCloud"
|
||||
:label="t('header.save_workspace')"
|
||||
filled
|
||||
class="hidden md:flex"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
v-if="currentUser === null"
|
||||
:label="t('header.login')"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
<div v-else class="inline-flex items-center space-x-2">
|
||||
<ButtonPrimary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.invite_tooltip')"
|
||||
:label="t('team.invite')"
|
||||
:icon="IconUserPlus"
|
||||
class="!bg-green-500 !bg-opacity-15 !text-green-500 !hover:bg-opacity-10 !hover:bg-green-400 !hover:text-green-600"
|
||||
@click="showTeamsModal = true"
|
||||
class="inline-flex items-center space-x-2"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconUploadCloud"
|
||||
:label="t('header.save_workspace')"
|
||||
class="hidden md:flex bg-green-500/15 py-1.75 border border-green-600/25 !text-green-500 hover:bg-green-400/10 focus-visible:bg-green-400/10 focus-visible:border-green-800/50 !focus-visible:text-green-600 hover:border-green-800/50 !hover:text-green-600"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
:label="t('header.login')"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="inline-flex items-center space-x-2">
|
||||
<TeamsMemberStack
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam.teamMembers.length > 1
|
||||
"
|
||||
:team-members="selectedTeam.teamMembers"
|
||||
show-count
|
||||
class="mx-2"
|
||||
@handle-click="handleTeamEdit()"
|
||||
/>
|
||||
<div
|
||||
class="flex border divide-x rounded bg-green-500/15 divide-green-600/25 border-green-600/25 focus-within:bg-green-400/10 focus-within:border-green-800/50 focus-within:divide-green-800/50 hover:bg-green-400/10 hover:border-green-800/50 hover:divide-green-800/50"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.invite_tooltip')"
|
||||
:icon="IconUserPlus"
|
||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
||||
@click="handleInvite()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam?.myRole === 'OWNER'
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.edit')"
|
||||
:icon="IconSettings"
|
||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
||||
@click="handleTeamEdit()"
|
||||
/>
|
||||
</div>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => accountActions.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('workspace.change')"
|
||||
:label="mdAndLarger ? workspaceName : ``"
|
||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||
class="pr-8 select-wrapper rounded bg-blue-500/15 py-1.75 border border-blue-600/25 !text-blue-500 focus-visible:bg-blue-400/10 focus-visible:border-blue-800/50 !focus-visible:text-blue-600 hover:bg-blue-400/10 hover:border-blue-800/50 !hover:text-blue-600"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="accountActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
@click="hide()"
|
||||
>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<span class="px-2">
|
||||
<tippy
|
||||
interactive
|
||||
@@ -127,7 +180,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="profile"
|
||||
to="/profile"
|
||||
:icon="IconUser"
|
||||
@@ -135,7 +188,7 @@
|
||||
:shortcut="['P']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="settings"
|
||||
to="/settings"
|
||||
:icon="IconSettings"
|
||||
@@ -157,24 +210,43 @@
|
||||
</header>
|
||||
<AppAnnouncement v-if="!network.isOnline" />
|
||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||
<TeamsInvite
|
||||
v-if="workspace.type === 'team' && workspace.teamID"
|
||||
:show="showModalInvite"
|
||||
:editing-team-i-d="editingTeamID"
|
||||
@hide-modal="displayModalInvite(false)"
|
||||
/>
|
||||
<TeamsEdit
|
||||
:show="showModalEdit"
|
||||
:editing-team="editingTeamName"
|
||||
:editing-team-i-d="editingTeamID"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||
@refetch-teams="refetchTeams"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconSearch from "~icons/lucide/search"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import IconSearch from "~icons/lucide/search"
|
||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -198,9 +270,108 @@ const currentUser = useReadonlyStream(
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
|
||||
const workspaceName = computed(() =>
|
||||
workspace.value.type === "personal"
|
||||
? t("workspace.personal")
|
||||
: workspace.value.teamName
|
||||
)
|
||||
|
||||
const refetchTeams = () => {
|
||||
teamListAdapter.fetchList()
|
||||
}
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
if (newTeams && workspace.value.type === "team" && workspace.value.teamID) {
|
||||
const team = newTeams.find((team) => team.id === workspace.value.teamID)
|
||||
if (team) {
|
||||
selectedTeam.value = team
|
||||
// Update the workspace name if it's not the same as the updated team name
|
||||
if (team.name !== workspace.value.teamName) {
|
||||
updateWorkspaceTeamName(workspace.value, team.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => workspace.value,
|
||||
(newWorkspace) => {
|
||||
if (newWorkspace.type === "team") {
|
||||
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
|
||||
if (team) {
|
||||
selectedTeam.value = team
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const showModalInvite = ref(false)
|
||||
const showModalEdit = ref(false)
|
||||
|
||||
const editingTeamName = ref<{ name: string }>({ name: "" })
|
||||
const editingTeamID = ref("")
|
||||
|
||||
const displayModalInvite = (show: boolean) => {
|
||||
showModalInvite.value = show
|
||||
}
|
||||
|
||||
const displayModalEdit = (show: boolean) => {
|
||||
showModalEdit.value = show
|
||||
teamListAdapter.fetchList()
|
||||
}
|
||||
|
||||
const inviteTeam = (team: { name: string }, teamID: string) => {
|
||||
editingTeamName.value = team
|
||||
editingTeamID.value = teamID
|
||||
displayModalInvite(true)
|
||||
}
|
||||
|
||||
// Show the workspace selected team invite modal if the user is an owner of the team else show the default invite modal
|
||||
const handleInvite = () => {
|
||||
if (
|
||||
workspace.value.type === "team" &&
|
||||
workspace.value.teamID &&
|
||||
selectedTeam.value?.myRole === "OWNER"
|
||||
) {
|
||||
editingTeamID.value = workspace.value.teamID
|
||||
displayModalInvite(true)
|
||||
} else {
|
||||
showTeamsModal.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Show the workspace selected team edit modal if the user is an owner of the team
|
||||
const handleTeamEdit = () => {
|
||||
if (
|
||||
workspace.value.type === "team" &&
|
||||
workspace.value.teamID &&
|
||||
selectedTeam.value?.myRole === "OWNER"
|
||||
) {
|
||||
editingTeamID.value = workspace.value.teamID
|
||||
editingTeamName.value = { name: selectedTeam.value.name }
|
||||
displayModalEdit(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const profile = ref<any | null>(null)
|
||||
const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
const accountActions = ref<any | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
{{ t("settings.interceptor_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<SmartRadioGroup v-model="interceptorSelection" :radios="interceptors" />
|
||||
<HoppSmartRadioGroup
|
||||
v-model="interceptorSelection"
|
||||
:radios="interceptors"
|
||||
/>
|
||||
<div
|
||||
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
||||
class="flex space-x-2"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||
blank
|
||||
:icon="IconChrome"
|
||||
@@ -21,7 +24,7 @@
|
||||
outline
|
||||
class="!flex-1"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||
blank
|
||||
:icon="IconFirefox"
|
||||
@@ -69,7 +72,7 @@ const interceptors = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
type InterceptorMode = typeof interceptors["value"][number]["value"]
|
||||
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
|
||||
|
||||
const interceptorSelection = computed<InterceptorMode>({
|
||||
get() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('app.options')"
|
||||
@@ -11,7 +11,7 @@
|
||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||
{{ t("layout.name") }}
|
||||
</h2>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconSidebar"
|
||||
:label="EXPAND_NAVIGATION ? t('hide.sidebar') : t('show.sidebar')"
|
||||
:description="t('layout.collapse_sidebar')"
|
||||
@@ -19,7 +19,7 @@
|
||||
active
|
||||
@click="expandNavigation"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconSidebarOpen"
|
||||
:label="SIDEBAR ? t('hide.collection') : t('show.collection')"
|
||||
:description="t('layout.collapse_collection')"
|
||||
@@ -30,7 +30,7 @@
|
||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||
{{ t("support.title") }}
|
||||
</h2>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconBook"
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
@@ -40,7 +40,7 @@
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="t('app.whats_new')"
|
||||
to="https://docs.hoppscotch.io/changelog"
|
||||
@@ -50,7 +50,7 @@
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconActivity"
|
||||
:label="t('app.status')"
|
||||
to="https://status.hoppscotch.io"
|
||||
@@ -60,7 +60,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconLock"
|
||||
:label="`${t('app.terms_and_privacy')}`"
|
||||
to="https://docs.hoppscotch.io/privacy"
|
||||
@@ -73,7 +73,7 @@
|
||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||
{{ t("settings.follow") }}
|
||||
</h2>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconDiscord"
|
||||
:label="t('app.discord')"
|
||||
to="https://hoppscotch.io/discord"
|
||||
@@ -83,7 +83,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconTwitter"
|
||||
:label="t('app.twitter')"
|
||||
to="https://hoppscotch.io/twitter"
|
||||
@@ -93,7 +93,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="`${t('app.github')}`"
|
||||
to="https://github.com/hoppscotch/hoppscotch"
|
||||
@@ -103,7 +103,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconMessageCircle"
|
||||
:label="t('app.chat_with_us')"
|
||||
:description="t('support.chat')"
|
||||
@@ -111,7 +111,7 @@
|
||||
active
|
||||
@click="chatWithUs()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconUserPlus"
|
||||
:label="`${t('app.invite')}`"
|
||||
:description="t('shortcut.miscellaneous.invite')"
|
||||
@@ -119,7 +119,7 @@
|
||||
active
|
||||
@click="expandInvite()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-if="navigatorShare"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconShare2"
|
||||
@@ -132,7 +132,7 @@
|
||||
</div>
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -145,7 +145,7 @@ import IconActivity from "~icons/lucide/activity"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconDiscord from "~icons/brands/discord"
|
||||
import IconTwitter from "~icons/brands/twitter"
|
||||
import IconGithub from "~icons/hopp/github"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<slot name="primary" />
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="hasSecondary"
|
||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||
class="flex flex-col !overflow-auto"
|
||||
>
|
||||
@@ -62,6 +63,7 @@ const SIDEBAR = useSetting("SIDEBAR")
|
||||
const slots = useSlots()
|
||||
|
||||
const hasSidebar = computed(() => !!slots.sidebar)
|
||||
const hasSecondary = computed(() => !!slots.secondary)
|
||||
|
||||
const props = defineProps({
|
||||
layoutId: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
styles="sm:max-w-lg"
|
||||
full-width
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('app.invite_your_friends')"
|
||||
@@ -35,7 +35,7 @@
|
||||
{{ t("app.invite_description") }}
|
||||
</p>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
|
||||
<HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
|
||||
<template #content>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||
@@ -76,7 +76,7 @@
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
</SmartSlideOver>
|
||||
</HoppSmartSlideOver>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('app.documentation')}`"
|
||||
to="https://docs.hoppscotch.io/features/response"
|
||||
:icon="IconExternalLink"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<aside class="flex justify-between h-full md:flex-col">
|
||||
<nav class="flex flex-1 flex-nowrap md:flex-col md:flex-none bg-primary">
|
||||
<SmartLink
|
||||
<HoppSmartLink
|
||||
v-for="(navigation, index) in primaryNavigation"
|
||||
:key="`navigation-${index}`"
|
||||
v-tippy="{
|
||||
@@ -20,7 +20,7 @@
|
||||
<span v-if="EXPAND_NAVIGATION" class="nav-title">
|
||||
{{ t(navigation.title) }}
|
||||
</span>
|
||||
</SmartLink>
|
||||
</HoppSmartLink>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('support.title')"
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconBook"
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
@@ -18,7 +18,7 @@
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconZap"
|
||||
:label="t('app.keyboard_shortcuts')"
|
||||
:description="t('support.shortcuts')"
|
||||
@@ -26,7 +26,7 @@
|
||||
active
|
||||
@click="showShortcuts()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGift"
|
||||
:label="t('app.whats_new')"
|
||||
to="https://docs.hoppscotch.io/changelog"
|
||||
@@ -36,7 +36,7 @@
|
||||
blank
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconMessageCircle"
|
||||
:label="t('app.chat_with_us')"
|
||||
:description="t('support.chat')"
|
||||
@@ -44,7 +44,7 @@
|
||||
active
|
||||
@click="chatWithUs()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGitHub"
|
||||
:label="t('app.github')"
|
||||
to="https://hoppscotch.io/github"
|
||||
@@ -54,7 +54,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconDiscord"
|
||||
:label="t('app.join_discord_community')"
|
||||
to="https://hoppscotch.io/discord"
|
||||
@@ -64,7 +64,7 @@
|
||||
active
|
||||
@click="hideModal()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconTwitter"
|
||||
:label="t('app.twitter')"
|
||||
to="https://hoppscotch.io/twitter"
|
||||
@@ -76,13 +76,13 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTwitter from "~icons/brands/twitter"
|
||||
import IconDiscord from "~icons/brands/discord"
|
||||
import IconGitHub from "~icons/hopp/github"
|
||||
import IconGitHub from "~icons/lucide/github"
|
||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('collection.new')"
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addNewCollection"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('folder.new')"
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addFolder"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('request.new')"
|
||||
@@ -22,13 +22,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -36,14 +36,14 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { getRESTRequest } from "~/newstore/RESTSession"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -70,7 +70,7 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
name.value = getRESTRequest().name
|
||||
name.value = currentActiveTab.value.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,140 +1,185 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="emit('toggle-children')"
|
||||
class="h-1 w-full transition"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': isReorderable,
|
||||
},
|
||||
]"
|
||||
@drop="orderUpdateCollectionEvent"
|
||||
@dragover.prevent="ordering = true"
|
||||
@dragleave="ordering = false"
|
||||
@dragend="resetDragState"
|
||||
></div>
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
|
||||
:class="{
|
||||
'opacity-25':
|
||||
dragging && notSameDestination && notSameParentDestination,
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
|
||||
:draggable="!hasNoTeamAccess"
|
||||
@dragstart="dragStart"
|
||||
@drop="handelDrop($event)"
|
||||
@dragover="handleDragOver($event)"
|
||||
@dragleave="resetDragState"
|
||||
@dragend="
|
||||
() => {
|
||||
resetDragState()
|
||||
dropItemID = ''
|
||||
}
|
||||
"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
>
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
|
||||
@click="emit('toggle-children')"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collectionName }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="!hasNoTeamAccess" class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-folder')"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
<div
|
||||
class="flex items-center justify-center flex-1 min-w-0"
|
||||
@click="emit('toggle-children')"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 pointer-events-none"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
<HoppSmartSpinner v-if="isCollLoading" />
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
v-else
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction?.$el.click()"
|
||||
@keyup.n="folderAction?.$el.click()"
|
||||
@keyup.e="edit?.$el.click()"
|
||||
@keyup.delete="deleteAction?.$el.click()"
|
||||
@keyup.x="exportAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-request')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-folder')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="
|
||||
() => {
|
||||
emit('export-data'),
|
||||
collectionsType === 'my-collections' ? hide() : null
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
emit('remove-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collectionName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!hasNoTeamAccess" class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-folder')"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="requestAction?.$el.click()"
|
||||
@keyup.n="folderAction?.$el.click()"
|
||||
@keyup.e="edit?.$el.click()"
|
||||
@keyup.delete="deleteAction?.$el.click()"
|
||||
@keyup.x="exportAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-request')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-folder')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="
|
||||
() => {
|
||||
emit('export-data'),
|
||||
collectionsType === 'my-collections' ? hide() : null
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
emit('remove-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isLastItem"
|
||||
class="w-full transition"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': isLastItemReorderable,
|
||||
'h-1 ': isLastItem,
|
||||
},
|
||||
]"
|
||||
@drop="updateLastItemOrder"
|
||||
@dragover.prevent="orderingLastItem = true"
|
||||
@dragleave="orderingLastItem = false"
|
||||
@dragend="resetDragState"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -153,6 +198,11 @@ import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import {
|
||||
changeCurrentReorderStatus,
|
||||
currentReorderingStatus$,
|
||||
} from "~/newstore/reordering"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
type CollectionType = "my-collections" | "team-collections"
|
||||
type FolderType = "collection" | "folder"
|
||||
@@ -160,6 +210,16 @@ type FolderType = "collection" | "folder"
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
parentID: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
||||
default: () => ({}),
|
||||
@@ -185,7 +245,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
type: Boolean as PropType<boolean | null>,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
@@ -199,6 +259,16 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
collectionMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
isLastItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -209,6 +279,10 @@ const emit = defineEmits<{
|
||||
(event: "export-data"): void
|
||||
(event: "remove-collection"): void
|
||||
(event: "drop-event", payload: DataTransfer): void
|
||||
(event: "drag-event", payload: DataTransfer): void
|
||||
(event: "dragging", payload: boolean): void
|
||||
(event: "update-collection-order", payload: DataTransfer): void
|
||||
(event: "update-last-collection-order", payload: DataTransfer): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -220,6 +294,28 @@ const exportAction = ref<HTMLButtonElement | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
const orderingLastItem = ref(false)
|
||||
const dropItemID = ref("")
|
||||
|
||||
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
type: "collection",
|
||||
id: "",
|
||||
parentID: "",
|
||||
})
|
||||
|
||||
// Used to determine if the collection is being dragged to a different destination
|
||||
// This is used to make the highlight effect work
|
||||
watch(
|
||||
() => dragging.value,
|
||||
(val) => {
|
||||
if (val && notSameDestination.value && notSameParentDestination.value) {
|
||||
emit("dragging", true)
|
||||
} else {
|
||||
emit("dragging", false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const collectionIcon = computed(() => {
|
||||
if (props.isSelected) return IconCheckCircle
|
||||
@@ -243,10 +339,125 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const dropEvent = ({ dataTransfer }: DragEvent) => {
|
||||
const notSameParentDestination = computed(() => {
|
||||
return currentReorderingStatus.value.parentID !== props.id
|
||||
})
|
||||
|
||||
const isRequestDragging = computed(() => {
|
||||
return currentReorderingStatus.value.type === "request"
|
||||
})
|
||||
|
||||
const isSameParent = computed(() => {
|
||||
return currentReorderingStatus.value.parentID === props.parentID
|
||||
})
|
||||
|
||||
const isReorderable = computed(() => {
|
||||
return (
|
||||
ordering.value &&
|
||||
notSameDestination.value &&
|
||||
!isRequestDragging.value &&
|
||||
isSameParent.value
|
||||
)
|
||||
})
|
||||
const isLastItemReorderable = computed(() => {
|
||||
return (
|
||||
orderingLastItem.value &&
|
||||
notSameDestination.value &&
|
||||
!isRequestDragging.value &&
|
||||
isSameParent.value
|
||||
)
|
||||
})
|
||||
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
emit("drag-event", dataTransfer)
|
||||
dropItemID.value = dataTransfer.getData("collectionIndex")
|
||||
dragging.value = !dragging.value
|
||||
emit("drop-event", dataTransfer)
|
||||
changeCurrentReorderStatus({
|
||||
type: "collection",
|
||||
id: props.id,
|
||||
parentID: props.parentID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the re-ordering event when a collection is dragged over another collection's top section
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
dragging.value = true
|
||||
if (
|
||||
e.offsetY < 10 &&
|
||||
notSameDestination.value &&
|
||||
!isRequestDragging.value &&
|
||||
isSameParent.value
|
||||
) {
|
||||
ordering.value = true
|
||||
dragging.value = false
|
||||
orderingLastItem.value = false
|
||||
} else if (
|
||||
e.offsetY > 18 &&
|
||||
notSameDestination.value &&
|
||||
!isRequestDragging.value &&
|
||||
isSameParent.value &&
|
||||
props.isLastItem
|
||||
) {
|
||||
orderingLastItem.value = true
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
} else {
|
||||
ordering.value = false
|
||||
orderingLastItem.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handelDrop = (e: DragEvent) => {
|
||||
if (ordering.value) {
|
||||
orderUpdateCollectionEvent(e)
|
||||
} else if (orderingLastItem.value) {
|
||||
updateLastItemOrder(e)
|
||||
} else {
|
||||
notSameParentDestination.value ? dropEvent(e) : e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
const dropEvent = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
emit("drop-event", e.dataTransfer)
|
||||
resetDragState()
|
||||
}
|
||||
}
|
||||
|
||||
const orderUpdateCollectionEvent = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
emit("update-collection-order", e.dataTransfer)
|
||||
resetDragState()
|
||||
}
|
||||
}
|
||||
|
||||
const updateLastItemOrder = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
emit("update-last-collection-order", e.dataTransfer)
|
||||
resetDragState()
|
||||
}
|
||||
}
|
||||
|
||||
const notSameDestination = computed(() => {
|
||||
return dropItemID.value !== props.id
|
||||
})
|
||||
|
||||
const isCollLoading = computed(() => {
|
||||
if (props.collectionMoveLoading.length > 0 && props.data.id) {
|
||||
return props.collectionMoveLoading.includes(props.data.id)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
orderingLastItem.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('collection.edit')"
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="saveCollection"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('folder.edit')"
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="editFolder"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.edit_request')"
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="editRequest"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.collections')"
|
||||
@@ -7,7 +7,7 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="importerType !== null"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.go_back')"
|
||||
@@ -101,7 +101,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="t('import.title')"
|
||||
:disabled="enableImportButton"
|
||||
:loading="importingMyCollections"
|
||||
@@ -109,9 +109,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<SmartExpand>
|
||||
<HoppSmartExpand>
|
||||
<template #body>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="(importer, index) in importerModules"
|
||||
:key="`importer-${index}`"
|
||||
:icon="importer.icon"
|
||||
@@ -119,10 +119,10 @@
|
||||
@click="importerType = index"
|
||||
/>
|
||||
</template>
|
||||
</SmartExpand>
|
||||
</HoppSmartExpand>
|
||||
<hr />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
@@ -131,6 +131,7 @@
|
||||
@click="emit('export-json-collection')"
|
||||
/>
|
||||
<span
|
||||
v-if="platform.platformFeatureFlags.exportAsGIST"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
!currentUser
|
||||
@@ -141,7 +142,7 @@
|
||||
"
|
||||
class="flex"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
@@ -158,7 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
@@ -8,21 +8,21 @@
|
||||
: 'top: var(--upper-primary-sticky-fold)'
|
||||
"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
class="!rounded-none"
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
<span class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/collections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
@@ -33,12 +33,17 @@
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<SmartTree :adapter="myAdapter">
|
||||
<template #content="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'collections'"
|
||||
:id="node.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
collectionIndex: parseInt(node.id),
|
||||
@@ -72,6 +77,22 @@
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: node.id,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@update-last-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: null,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) => highlightChildren(isDraging ? node.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -85,9 +106,12 @@
|
||||
/>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'folders'"
|
||||
:id="node.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
folderPath: node.id,
|
||||
@@ -121,6 +145,22 @@
|
||||
"
|
||||
@remove-collection="emit('remove-folder', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: node.id,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@update-last-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: null,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) => highlightChildren(isDraging ? node.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -135,8 +175,11 @@
|
||||
<CollectionsRequest
|
||||
v-if="node.data.type === 'requests'"
|
||||
:request="node.data.data.data"
|
||||
:request-i-d="node.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:collections-type="collectionsType.type"
|
||||
:save-request="saveRequest"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-active="
|
||||
isActiveRequest(
|
||||
node.data.data.parentIndex,
|
||||
@@ -182,7 +225,19 @@
|
||||
@drag-request="
|
||||
dragRequest($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: pathToIndex(node.id),
|
||||
requestIndex: node.id,
|
||||
})
|
||||
"
|
||||
@update-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.id,
|
||||
})
|
||||
"
|
||||
@update-last-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: null,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -210,7 +265,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@@ -231,7 +286,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collection") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@@ -274,14 +329,14 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { restSaveContext$ } from "~/newstore/RESTSession"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
|
||||
export type Collection = {
|
||||
type: "collections"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: null
|
||||
data: HoppCollection<HoppRESTRequest>
|
||||
@@ -290,6 +345,7 @@ export type Collection = {
|
||||
|
||||
type Folder = {
|
||||
type: "folders"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: string
|
||||
data: HoppCollection<HoppRESTRequest>
|
||||
@@ -298,6 +354,7 @@ type Folder = {
|
||||
|
||||
type Requests = {
|
||||
type: "requests"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: string
|
||||
data: HoppRESTRequest
|
||||
@@ -413,7 +470,32 @@ const emit = defineEmits<{
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
collectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-collection",
|
||||
payload: {
|
||||
collectionIndexDragged: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-request-order",
|
||||
payload: {
|
||||
dragedRequestIndex: string
|
||||
destinationRequestIndex: string | null
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-collection-order",
|
||||
payload: {
|
||||
dragedCollectionIndex: string
|
||||
destinationCollection: {
|
||||
destinationCollectionIndex: string | null
|
||||
destinationCollectionParentIndex: string | null
|
||||
}
|
||||
}
|
||||
): void
|
||||
(event: "select", payload: Picked | null): void
|
||||
@@ -422,63 +504,57 @@ const emit = defineEmits<{
|
||||
|
||||
const refFilterCollection = toRef(props, "filteredCollections")
|
||||
|
||||
const pathToIndex = computed(() => {
|
||||
return (path: string) => {
|
||||
const pathArr = path.split("/")
|
||||
return pathArr[pathArr.length - 1]
|
||||
}
|
||||
})
|
||||
const pathToIndex = (path: string) => {
|
||||
const pathArr = path.split("/")
|
||||
return pathArr[pathArr.length - 1]
|
||||
}
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return ({
|
||||
collectionIndex,
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: {
|
||||
collectionIndex?: number | undefined
|
||||
folderPath?: string | undefined
|
||||
requestIndex?: number | undefined
|
||||
}) => {
|
||||
if (collectionIndex !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-collection" &&
|
||||
props.picked.collectionIndex === collectionIndex
|
||||
)
|
||||
} else if (requestIndex !== undefined && folderPath !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-request" &&
|
||||
props.picked.folderPath === folderPath &&
|
||||
props.picked.requestIndex === requestIndex
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-folder" &&
|
||||
props.picked.folderPath === folderPath
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const active = useReadonlyStream(restSaveContext$, null)
|
||||
|
||||
const isActiveRequest = computed(() => {
|
||||
return (folderPath: string, requestIndex: number) => {
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
O.filter(
|
||||
(active) =>
|
||||
active.originLocation === "user-collection" &&
|
||||
active.folderPath === folderPath &&
|
||||
active.requestIndex === requestIndex
|
||||
),
|
||||
O.isSome
|
||||
const isSelected = ({
|
||||
collectionIndex,
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: {
|
||||
collectionIndex?: number | undefined
|
||||
folderPath?: string | undefined
|
||||
requestIndex?: number | undefined
|
||||
}) => {
|
||||
if (collectionIndex !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-collection" &&
|
||||
props.picked.collectionIndex === collectionIndex
|
||||
)
|
||||
} else if (requestIndex !== undefined && folderPath !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-request" &&
|
||||
props.picked.folderPath === folderPath &&
|
||||
props.picked.requestIndex === requestIndex
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-folder" &&
|
||||
props.picked.folderPath === folderPath
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||
|
||||
const isActiveRequest = (folderPath: string, requestIndex: number) => {
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
O.filter(
|
||||
(active) =>
|
||||
active.originLocation === "user-collection" &&
|
||||
active.folderPath === folderPath &&
|
||||
active.requestIndex === requestIndex
|
||||
),
|
||||
O.isSome
|
||||
)
|
||||
}
|
||||
|
||||
const selectRequest = (data: {
|
||||
request: HoppRESTRequest
|
||||
@@ -486,6 +562,7 @@ const selectRequest = (data: {
|
||||
requestIndex: string
|
||||
}) => {
|
||||
const { request, folderPath, requestIndex } = data
|
||||
|
||||
if (props.saveRequest) {
|
||||
emit("select", {
|
||||
pickedType: "my-request",
|
||||
@@ -497,11 +574,15 @@ const selectRequest = (data: {
|
||||
request,
|
||||
folderPath,
|
||||
requestIndex,
|
||||
isActive: isActiveRequest.value(folderPath, parseInt(requestIndex)),
|
||||
isActive: isActiveRequest(folderPath, parseInt(requestIndex)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
dataTransfer.setData("collectionIndex", collectionIndex)
|
||||
}
|
||||
|
||||
const dragRequest = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
@@ -514,13 +595,59 @@ const dragRequest = (
|
||||
dataTransfer.setData("requestIndex", requestIndex)
|
||||
}
|
||||
|
||||
const dropEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
const dropEvent = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
emit("drop-request", {
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
collectionIndexDragged,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRequestOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
collectionIndex,
|
||||
}: { folderPath: string | null; requestIndex: string | null }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
const dragedRequestIndex = dataTransfer.getData("requestIndex")
|
||||
const destinationRequestIndex = requestIndex
|
||||
const destinationCollectionIndex = folderPath
|
||||
|
||||
emit("update-request-order", {
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCollectionOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollection: {
|
||||
destinationCollectionIndex: string | null
|
||||
destinationCollectionParentIndex: string | null
|
||||
}
|
||||
) => {
|
||||
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
|
||||
|
||||
emit("update-collection-order", {
|
||||
dragedCollectionIndex,
|
||||
destinationCollection,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -550,6 +677,7 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
id: index.toString(),
|
||||
data: {
|
||||
type: "collections",
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
data: {
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
@@ -571,23 +699,31 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
|
||||
if (item) {
|
||||
const data = [
|
||||
...item.folders.map((item, index) => ({
|
||||
...item.folders.map((folder, index) => ({
|
||||
id: `${id}/${index}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
item.folders && item.folders.length > 1
|
||||
? index === item.folders.length - 1
|
||||
: false,
|
||||
type: "folders",
|
||||
data: {
|
||||
parentIndex: id,
|
||||
data: item,
|
||||
data: folder,
|
||||
},
|
||||
},
|
||||
})),
|
||||
...item.requests.map((item, index) => ({
|
||||
...item.requests.map((requests, index) => ({
|
||||
id: `${id}/${index}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
item.requests && item.requests.length > 1
|
||||
? index === item.requests.length - 1
|
||||
: false,
|
||||
type: "requests",
|
||||
data: {
|
||||
parentIndex: id,
|
||||
data: item,
|
||||
data: requests,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
@@ -1,53 +1,70 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="h-1 w-full transition"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': isReorderable,
|
||||
},
|
||||
]"
|
||||
@drop="updateRequestOrder"
|
||||
@dragover.prevent="ordering = true"
|
||||
@dragleave="resetDragState"
|
||||
@dragend="resetDragState"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
draggable="true"
|
||||
:draggable="!hasNoTeamAccess"
|
||||
@drop="handelDrop"
|
||||
@dragstart="dragStart"
|
||||
@dragover.stop
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@dragover="handleDragOver($event)"
|
||||
@dragleave="resetDragState"
|
||||
@dragend="resetDragState"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||
:class="requestLabelColor"
|
||||
<div
|
||||
class="flex items-center justify-center flex-1 min-w-0 cursor-pointer pointer-events-auto"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<component
|
||||
:is="IconCheckCircle"
|
||||
v-if="isSelected"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<span v-else class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
||||
:class="requestLabelColor"
|
||||
>
|
||||
<component
|
||||
:is="IconCheckCircle"
|
||||
v-if="isSelected"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<HoppSmartSpinner v-else-if="isRequestLoading" />
|
||||
<span v-else class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</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')}`"
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 pointer-events-none transition group-hover:text-secondaryDark"
|
||||
>
|
||||
<span
|
||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||
></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>
|
||||
<div v-if="!hasNoTeamAccess" class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconRotateCCW"
|
||||
@@ -63,7 +80,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -78,7 +95,7 @@
|
||||
@keyup.delete="deleteAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
@@ -90,7 +107,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="t('action.duplicate')"
|
||||
@@ -103,7 +120,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
@@ -121,6 +138,19 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full transition"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': isLastItemReorderable,
|
||||
'h-1 ': isLastItem,
|
||||
},
|
||||
]"
|
||||
@drop="handelDrop"
|
||||
@dragover.prevent="orderingLastItem = true"
|
||||
@dragleave="resetDragState"
|
||||
@dragend="resetDragState"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -135,9 +165,12 @@ import { ref, PropType, watch, computed } from "vue"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as RR from "fp-ts/ReadonlyRecord"
|
||||
import * as O from "fp-ts/Option"
|
||||
import {
|
||||
changeCurrentReorderStatus,
|
||||
currentReorderingStatus$,
|
||||
} from "~/newstore/reordering"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
|
||||
type CollectionType = "my-collections" | "team-collections"
|
||||
|
||||
@@ -149,6 +182,16 @@ const props = defineProps({
|
||||
default: () => ({}),
|
||||
required: true,
|
||||
},
|
||||
requestID: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
parentID: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
collectionsType: {
|
||||
type: String as PropType<CollectionType>,
|
||||
default: "my-collections",
|
||||
@@ -175,6 +218,16 @@ const props = defineProps({
|
||||
required: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean as PropType<boolean | null>,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
requestMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
isLastItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
@@ -187,6 +240,8 @@ const emit = defineEmits<{
|
||||
(event: "remove-request"): void
|
||||
(event: "select-request"): void
|
||||
(event: "drag-request", payload: DataTransfer): void
|
||||
(event: "update-request-order", payload: DataTransfer): void
|
||||
(event: "update-last-request-order", payload: DataTransfer): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -196,21 +251,17 @@ const options = ref<TippyComponent | null>(null)
|
||||
const duplicate = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
const orderingLastItem = ref(false)
|
||||
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
post: "text-yellow-500",
|
||||
put: "text-blue-500",
|
||||
delete: "text-red-500",
|
||||
default: "text-gray-500",
|
||||
} as const
|
||||
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
type: "collection",
|
||||
id: "",
|
||||
parentID: "",
|
||||
})
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
pipe(
|
||||
requestMethodLabels,
|
||||
RR.lookup(props.request.method.toLowerCase()),
|
||||
O.getOrElseW(() => requestMethodLabels.default)
|
||||
)
|
||||
getMethodLabelColorClassOf(props.request)
|
||||
)
|
||||
|
||||
watch(
|
||||
@@ -228,8 +279,97 @@ const selectRequest = () => {
|
||||
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
dragging.value = !dragging.value
|
||||
emit("drag-request", dataTransfer)
|
||||
dragging.value = !dragging.value
|
||||
changeCurrentReorderStatus({
|
||||
type: "request",
|
||||
id: props.requestID,
|
||||
parentID: props.parentID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const isSameRequest = computed(() => {
|
||||
return currentReorderingStatus.value.id === props.requestID
|
||||
})
|
||||
|
||||
const isCollectionDragging = computed(() => {
|
||||
return currentReorderingStatus.value.type === "collection"
|
||||
})
|
||||
|
||||
const isSameParent = computed(() => {
|
||||
return currentReorderingStatus.value.parentID === props.parentID
|
||||
})
|
||||
|
||||
const isReorderable = computed(() => {
|
||||
return (
|
||||
ordering.value &&
|
||||
!isCollectionDragging.value &&
|
||||
isSameParent.value &&
|
||||
!isSameRequest.value
|
||||
)
|
||||
})
|
||||
|
||||
const isLastItemReorderable = computed(() => {
|
||||
return (
|
||||
orderingLastItem.value && isSameParent.value && !isCollectionDragging.value
|
||||
)
|
||||
})
|
||||
|
||||
// Trigger the re-ordering event when a request is dragged over another request's top section
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
dragging.value = true
|
||||
if (e.offsetY < 10) {
|
||||
ordering.value = true
|
||||
dragging.value = false
|
||||
orderingLastItem.value = false
|
||||
} else if (e.offsetY > 18) {
|
||||
orderingLastItem.value = true
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
} else {
|
||||
ordering.value = false
|
||||
orderingLastItem.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handelDrop = (e: DragEvent) => {
|
||||
if (ordering.value) {
|
||||
updateRequestOrder(e)
|
||||
} else if (orderingLastItem.value) {
|
||||
updateLastItemOrder(e)
|
||||
} else {
|
||||
updateRequestOrder(e)
|
||||
}
|
||||
}
|
||||
|
||||
const updateRequestOrder = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
resetDragState()
|
||||
emit("update-request-order", e.dataTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
const updateLastItemOrder = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
resetDragState()
|
||||
emit("update-last-request-order", e.dataTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
const isRequestLoading = computed(() => {
|
||||
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
||||
return props.requestMoveLoading.includes(props.requestID)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
orderingLastItem.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('collection.save_as')}`"
|
||||
@@ -43,13 +44,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
:loading="modalLoadingState"
|
||||
outline
|
||||
@click="saveRequestAs"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -57,12 +58,12 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
HoppGQLRequest,
|
||||
HoppRESTRequest,
|
||||
@@ -70,8 +71,6 @@ import {
|
||||
} from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
createRequestInCollection,
|
||||
@@ -79,11 +78,8 @@ import {
|
||||
} from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||
import {
|
||||
getRESTRequest,
|
||||
setRESTSaveContext,
|
||||
useRESTRequestName,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
editGraphqlRequest,
|
||||
editRESTRequest,
|
||||
@@ -91,6 +87,8 @@ import {
|
||||
saveRESTRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -127,8 +125,13 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const requestName = ref(
|
||||
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
|
||||
const gqlRequestName = useGQLRequestName()
|
||||
const requestName = computedWithControl(
|
||||
() => [currentActiveTab.value, gqlRequestName.value],
|
||||
() =>
|
||||
props.mode === "rest"
|
||||
? currentActiveTab.value.document.request.name
|
||||
: gqlRequestName.value
|
||||
)
|
||||
|
||||
const requestData = reactive({
|
||||
@@ -186,7 +189,7 @@ const saveRequestAs = async () => {
|
||||
|
||||
const requestUpdated =
|
||||
props.mode === "rest"
|
||||
? cloneDeep(getRESTRequest())
|
||||
? cloneDeep(currentActiveTab.value.document.request)
|
||||
: cloneDeep(getGQLSession().request)
|
||||
|
||||
if (picked.value.pickedType === "my-collection") {
|
||||
@@ -198,12 +201,15 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: `${picked.value.collectionIndex}`,
|
||||
requestIndex: insertionIndex,
|
||||
req: requestUpdated,
|
||||
})
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: `${picked.value.collectionIndex}`,
|
||||
requestIndex: insertionIndex,
|
||||
},
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-folder") {
|
||||
@@ -215,12 +221,15 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: insertionIndex,
|
||||
req: requestUpdated,
|
||||
})
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: insertionIndex,
|
||||
},
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-request") {
|
||||
@@ -233,12 +242,15 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: picked.value.requestIndex,
|
||||
req: requestUpdated,
|
||||
})
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: picked.value.folderPath,
|
||||
requestIndex: picked.value.requestIndex,
|
||||
},
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "teams-collection") {
|
||||
@@ -341,13 +353,17 @@ const updateTeamCollectionOrFolder = (
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: createRequestInCollection.id,
|
||||
collectionID: createRequestInCollection.collection.id,
|
||||
teamID: createRequestInCollection.collection.team.id,
|
||||
req: requestUpdated,
|
||||
})
|
||||
currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
originLocation: "team-collection",
|
||||
requestID: createRequestInCollection.id,
|
||||
collectionID: createRequestInCollection.collection.id,
|
||||
teamID: createRequestInCollection.collection.team.id,
|
||||
},
|
||||
}
|
||||
|
||||
modalLoadingState.value = false
|
||||
requestSaved()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
saveRequest
|
||||
? 'top: calc(var(--upper-secondary-sticky-fold) - var(--line-height-body))'
|
||||
: 'top: var(--upper-secondary-sticky-fold)'
|
||||
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
||||
: 'top: var(--upper-primary-sticky-fold)'
|
||||
"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
@@ -17,7 +17,7 @@
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
@@ -25,14 +25,14 @@
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
<span class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/collections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="
|
||||
@@ -47,14 +47,20 @@
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<SmartTree :adapter="teamAdapter">
|
||||
<template #content="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'collections'"
|
||||
:id="node.data.data.data.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
collectionID: node.id,
|
||||
@@ -87,6 +93,24 @@
|
||||
emit('export-data', node.data.data.data)
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: node.data.data.data.id,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@update-last-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: null,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) =>
|
||||
highlightChildren(isDraging ? node.data.data.data.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -100,11 +124,15 @@
|
||||
/>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'folders'"
|
||||
:id="node.data.data.data.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
folderID: node.data.data.data.id,
|
||||
@@ -139,6 +167,24 @@
|
||||
node.data.type === 'folders' &&
|
||||
emit('remove-folder', node.data.data.data.id)
|
||||
"
|
||||
@drop-event="dropEvent($event, node.data.data.data.id)"
|
||||
@drag-event="dragEvent($event, node.data.data.data.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: node.data.data.data.id,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@update-last-collection-order="
|
||||
updateCollectionOrder($event, {
|
||||
destinationCollectionIndex: null,
|
||||
destinationCollectionParentIndex: node.data.data.parentIndex,
|
||||
})
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) =>
|
||||
highlightChildren(isDraging ? node.data.data.data.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -153,10 +199,14 @@
|
||||
<CollectionsRequest
|
||||
v-if="node.data.type === 'requests'"
|
||||
:request="node.data.data.data.request"
|
||||
:request-i-d="node.data.data.data.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:collections-type="collectionsType.type"
|
||||
:duplicate-loading="duplicateLoading"
|
||||
:is-active="isActiveRequest(node.data.data.data.id)"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:request-move-loading="requestMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
requestID: node.data.data.data.id,
|
||||
@@ -190,34 +240,54 @@
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@drag-request="
|
||||
dragRequest($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@update-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@update-last-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: null,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<div v-if="node === null">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
outline
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
@@ -227,6 +297,7 @@
|
||||
<div
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
@@ -235,34 +306,13 @@
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collection") }}
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
outline
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
@@ -293,11 +343,10 @@ import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { restSaveContext$ } from "~/newstore/RESTSession"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -347,6 +396,16 @@ const props = defineProps({
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
collectionMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
requestMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -410,6 +469,39 @@ const emit = defineEmits<{
|
||||
folderPath?: string | undefined
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-request",
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-collection",
|
||||
payload: {
|
||||
collectionIndexDragged: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-request-order",
|
||||
payload: {
|
||||
dragedRequestIndex: string
|
||||
destinationRequestIndex: string | null
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-collection-order",
|
||||
payload: {
|
||||
dragedCollectionIndex: string
|
||||
destinationCollection: {
|
||||
destinationCollectionIndex: string | null
|
||||
destinationCollectionParentIndex: string | null
|
||||
}
|
||||
}
|
||||
): void
|
||||
(event: "select", payload: Picked | null): void
|
||||
(event: "expand-team-collection", payload: string): void
|
||||
(event: "display-modal-add"): void
|
||||
@@ -425,54 +517,50 @@ const hasNoTeamAccess = computed(
|
||||
props.collectionsType.selectedTeam.myRole === "VIEWER")
|
||||
)
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return ({
|
||||
collectionID,
|
||||
folderID,
|
||||
requestID,
|
||||
}: {
|
||||
collectionID?: string | undefined
|
||||
folderID?: string | undefined
|
||||
requestID?: string | undefined
|
||||
}) => {
|
||||
if (collectionID !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-collection" &&
|
||||
props.picked.collectionID === collectionID
|
||||
)
|
||||
} else if (requestID !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-request" &&
|
||||
props.picked.requestID === requestID
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-folder" &&
|
||||
props.picked.folderID === folderID
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const active = useReadonlyStream(restSaveContext$, null)
|
||||
|
||||
const isActiveRequest = computed(() => {
|
||||
return (requestID: string) => {
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
O.filter(
|
||||
(active) =>
|
||||
active.originLocation === "team-collection" &&
|
||||
active.requestID === requestID
|
||||
),
|
||||
O.isSome
|
||||
const isSelected = ({
|
||||
collectionID,
|
||||
folderID,
|
||||
requestID,
|
||||
}: {
|
||||
collectionID?: string | undefined
|
||||
folderID?: string | undefined
|
||||
requestID?: string | undefined
|
||||
}) => {
|
||||
if (collectionID !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-collection" &&
|
||||
props.picked.collectionID === collectionID
|
||||
)
|
||||
} else if (requestID !== undefined) {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-request" &&
|
||||
props.picked.requestID === requestID
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-folder" &&
|
||||
props.picked.folderID === folderID
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||
|
||||
const isActiveRequest = (requestID: string) => {
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
O.filter(
|
||||
(active) =>
|
||||
active.originLocation === "team-collection" &&
|
||||
active.requestID === requestID
|
||||
),
|
||||
O.isSome
|
||||
)
|
||||
}
|
||||
|
||||
const selectRequest = (data: {
|
||||
request: HoppRESTRequest
|
||||
@@ -488,12 +576,84 @@ const selectRequest = (data: {
|
||||
emit("select-request", {
|
||||
request: request,
|
||||
requestIndex: requestIndex,
|
||||
isActive: isActiveRequest.value(requestIndex),
|
||||
isActive: isActiveRequest(requestIndex),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dragRequest = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: { folderPath: string | null; requestIndex: string }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
dataTransfer.setData("folderPath", folderPath)
|
||||
dataTransfer.setData("requestIndex", requestIndex)
|
||||
}
|
||||
|
||||
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
dataTransfer.setData("collectionIndex", collectionIndex)
|
||||
}
|
||||
|
||||
const dropEvent = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
collectionIndexDragged,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRequestOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: { folderPath: string | null; requestIndex: string | null }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
const dragedRequestIndex = dataTransfer.getData("requestIndex")
|
||||
const destinationRequestIndex = requestIndex
|
||||
const destinationCollectionIndex = folderPath
|
||||
|
||||
emit("update-request-order", {
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCollectionOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollection: {
|
||||
destinationCollectionIndex: string | null
|
||||
destinationCollectionParentIndex: string | null
|
||||
}
|
||||
) => {
|
||||
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
|
||||
|
||||
emit("update-collection-order", {
|
||||
dragedCollectionIndex,
|
||||
destinationCollection,
|
||||
})
|
||||
}
|
||||
|
||||
type TeamCollections = {
|
||||
isLastItem: boolean
|
||||
type: "collections"
|
||||
data: {
|
||||
parentIndex: null
|
||||
@@ -502,6 +662,7 @@ type TeamCollections = {
|
||||
}
|
||||
|
||||
type TeamFolder = {
|
||||
isLastItem: boolean
|
||||
type: "folders"
|
||||
data: {
|
||||
parentIndex: string
|
||||
@@ -510,6 +671,7 @@ type TeamFolder = {
|
||||
}
|
||||
|
||||
type TeamRequests = {
|
||||
isLastItem: boolean
|
||||
type: "requests"
|
||||
data: {
|
||||
parentIndex: string
|
||||
@@ -549,9 +711,10 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||
status: "loading",
|
||||
}
|
||||
} else {
|
||||
const data = this.data.value.map((item) => ({
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
data: {
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
type: "collections",
|
||||
data: {
|
||||
parentIndex: null,
|
||||
@@ -579,9 +742,13 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||
if (items) {
|
||||
const data = [
|
||||
...(items.children
|
||||
? items.children.map((item) => ({
|
||||
? items.children.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.children && items.children.length > 1
|
||||
? index === items.children.length - 1
|
||||
: false,
|
||||
type: "folders",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
@@ -591,9 +758,13 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||
}))
|
||||
: []),
|
||||
...(items.requests
|
||||
? items.requests.map((item) => ({
|
||||
? items.requests.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.requests && items.requests.length > 1
|
||||
? index === items.requests.length - 1
|
||||
: false,
|
||||
type: "requests",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-1">
|
||||
<SmartIntersection
|
||||
class="flex flex-col flex-1"
|
||||
@intersecting="onTeamSelectIntersect"
|
||||
>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
placement="bottom"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('collection.select_team')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-if="collectionsType.selectedTeam"
|
||||
:icon="IconUsers"
|
||||
:label="collectionsType.selectedTeam.name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="`${t('collection.select_team')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<div
|
||||
v-if="isTeamListLoading && myTeams.length === 0"
|
||||
class="flex flex-col items-center justify-center flex-1 p-2"
|
||||
>
|
||||
<SmartSpinner class="my-2" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-else-if="myTeams.length > 0" class="flex flex-col">
|
||||
<SmartItem
|
||||
v-for="(team, index) in myTeams"
|
||||
:key="`team-${index}`"
|
||||
:label="team.name"
|
||||
:info-icon="
|
||||
team.id === collectionsType.selectedTeam?.id
|
||||
? IconDone
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="team.id === collectionsType.selectedTeam?.id"
|
||||
:icon="IconUsers"
|
||||
@click="
|
||||
() => {
|
||||
updateSelectedTeam(team)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
:icon="IconPlus"
|
||||
:label="t('team.create_new')"
|
||||
@click="
|
||||
() => {
|
||||
displayTeamModalAdd(true)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center p-2 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center mb-4 w-14 h-14"
|
||||
:alt="`${t('empty.teams')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.teams") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="t('team.create_new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
displayTeamModalAdd(true)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</SmartIntersection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import { PropType, ref } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
type CollectionType =
|
||||
| {
|
||||
type: "team-collections"
|
||||
selectedTeam: SelectedTeam
|
||||
}
|
||||
| { type: "my-collections"; selectedTeam: undefined }
|
||||
|
||||
defineProps({
|
||||
collectionsType: {
|
||||
type: Object as PropType<CollectionType>,
|
||||
default: () => ({ type: "my-collections", selectedTeam: undefined }),
|
||||
required: true,
|
||||
},
|
||||
myTeams: {
|
||||
type: Array as PropType<GetMyTeamsQuery["myTeams"]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
isTeamListLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update-selected-team", payload: SelectedTeam): void
|
||||
(e: "team-select-intersect", payload: boolean): void
|
||||
(e: "display-team-modal-add", payload: boolean): void
|
||||
}>()
|
||||
|
||||
const updateSelectedTeam = (team: SelectedTeam) => {
|
||||
emit("update-selected-team", team)
|
||||
}
|
||||
|
||||
const onTeamSelectIntersect = () => {
|
||||
emit("team-select-intersect", true)
|
||||
}
|
||||
|
||||
const displayTeamModalAdd = (display: boolean) => {
|
||||
emit("display-team-modal-add", display)
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('collection.new')}`"
|
||||
@@ -24,12 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="addNewCollection"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('folder.new')"
|
||||
@@ -24,8 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary :label="t('action.save')" outline @click="addFolder" />
|
||||
<ButtonSecondary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="addFolder"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -33,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('request.new')"
|
||||
@@ -24,8 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary :label="t('action.save')" outline @click="addRequest" />
|
||||
<ButtonSecondary
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@@ -33,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
@@ -40,7 +40,7 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
@@ -59,7 +59,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -75,7 +75,7 @@
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="`${t('request.new')}`"
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="`${t('folder.new')}`"
|
||||
@@ -103,7 +103,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('action.edit')}`"
|
||||
@@ -115,7 +115,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
@@ -186,7 +186,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collection") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@@ -199,7 +199,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_collection')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@@ -299,7 +299,8 @@ const removeCollection = () => {
|
||||
) {
|
||||
emit("select", null)
|
||||
}
|
||||
removeGraphqlCollection(props.collectionIndex)
|
||||
|
||||
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('collection.edit')}`"
|
||||
@@ -24,12 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveCollection"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('folder.edit')}`"
|
||||
@@ -24,12 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="editFolder"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('modal.edit_request')}`"
|
||||
@@ -24,12 +24,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request', { path: folderPath })"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
@@ -51,7 +51,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -67,7 +67,7 @@
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="`${t('request.new')}`"
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="`${t('folder.new')}`"
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('action.edit')}`"
|
||||
@@ -103,7 +103,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -123,7 +123,7 @@
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
@@ -181,7 +181,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_folder')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@@ -279,7 +279,7 @@ const removeFolder = () => {
|
||||
emit("select", { picked: null })
|
||||
}
|
||||
|
||||
removeGraphqlFolder(props.folderPath)
|
||||
removeGraphqlFolder(props.folderPath, props.folder.id)
|
||||
toast.success(t("state.deleted"))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('modal.collections')}`"
|
||||
@@ -9,7 +9,7 @@
|
||||
<template #actions>
|
||||
<span>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -22,7 +22,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="t('import.from_gist')"
|
||||
@click="
|
||||
@@ -42,7 +42,7 @@
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
@@ -67,7 +67,7 @@
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('import.from_json')"
|
||||
@click="openDialogChooseFileToImportFrom"
|
||||
@@ -80,7 +80,7 @@
|
||||
@change="importFromJSON"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
@@ -89,7 +89,7 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconRotateCCW"
|
||||
@@ -44,7 +44,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -59,7 +59,7 @@
|
||||
@keyup.delete="deleteAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('action.edit')}`"
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -109,7 +109,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_request')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@@ -214,7 +214,7 @@ const removeRequest = () => {
|
||||
emit("select", null)
|
||||
}
|
||||
|
||||
removeGraphqlRequest(props.folderPath, props.requestIndex)
|
||||
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,21 +16,21 @@
|
||||
<div
|
||||
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
class="!rounded-none"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/collections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('modal.import_export')"
|
||||
@@ -73,7 +73,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,169 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<SmartTabs
|
||||
:id="'environments_tab'"
|
||||
v-model="selectedEnvironmentTab"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_environments')}`"
|
||||
/>
|
||||
<SmartTab
|
||||
:id="'team-environments'"
|
||||
:label="`${t('environment.team_environments')}`"
|
||||
>
|
||||
<SmartIntersection @intersecting="onTeamSelectIntersect">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
placement="bottom"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('collection.select_team')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-if="environmentType.selectedTeam"
|
||||
:icon="IconUsers"
|
||||
:label="environmentType.selectedTeam.name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
:label="`${t('collection.select_team')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
v-for="(team, index) in myTeams"
|
||||
:key="`team-${index}`"
|
||||
:label="team.name"
|
||||
:info-icon="
|
||||
team.id === environmentType.selectedTeam?.id
|
||||
? IconDone
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="
|
||||
team.id === environmentType.selectedTeam?.id
|
||||
"
|
||||
:icon="IconUsers"
|
||||
@click="
|
||||
() => {
|
||||
updateSelectedTeam(team)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</SmartIntersection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import { platform } from "~/platform"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type TeamData = GetMyTeamsQuery["myTeams"][number]
|
||||
|
||||
type SelectedTeam = TeamData | undefined
|
||||
|
||||
type EnvironmentTabs = "my-environments" | "team-environments"
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const selectedEnvironmentTab = ref<EnvironmentTabs>("my-environments")
|
||||
|
||||
defineProps<{
|
||||
environmentType: {
|
||||
type: "my-environments" | "team-environments"
|
||||
selectedTeam: SelectedTeam
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update-environment-type", tabID: EnvironmentTabs): void
|
||||
(e: "update-selected-team", team: SelectedTeam): void
|
||||
}>()
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const adapter = new TeamListAdapter(true)
|
||||
const myTeams = useReadonlyStream(adapter.teamList$, null)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
let teamListFetched = false
|
||||
|
||||
watch(myTeams, (teams) => {
|
||||
if (teams && !teamListFetched) {
|
||||
teamListFetched = true
|
||||
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
|
||||
const team = teams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||
if (team) updateSelectedTeam(team)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentUser.value,
|
||||
(user) => {
|
||||
if (!user) {
|
||||
selectedEnvironmentTab.value = "my-environments"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
const onTeamSelectIntersect = () => {
|
||||
// Load team data as soon as intersection
|
||||
adapter.fetchList()
|
||||
}
|
||||
|
||||
const updateEnvironmentType = (tabID: EnvironmentTabs) => {
|
||||
emit("update-environment-type", tabID)
|
||||
}
|
||||
|
||||
const updateSelectedTeam = (team: SelectedTeam) => {
|
||||
REMEMBERED_TEAM_ID.value = team?.id
|
||||
emit("update-selected-team", team)
|
||||
}
|
||||
|
||||
watch(selectedEnvironmentTab, (newValue: EnvironmentTabs) => {
|
||||
if (newValue === "team-environments" && !currentUser.value) {
|
||||
invokeAction("modals.login.toggle")
|
||||
nextTick(() => (selectedEnvironmentTab.value = "my-environments"))
|
||||
} else updateEnvironmentType(newValue)
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('environment.title')}`"
|
||||
@@ -14,7 +14,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -26,7 +26,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="t('import.from_gist')"
|
||||
@click="
|
||||
@@ -46,7 +46,7 @@
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
@@ -71,11 +71,11 @@
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||
<SmartSpinner class="my-4" />
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('import.from_json')"
|
||||
@click="openDialogChooseFileToImportFrom"
|
||||
@@ -88,7 +88,7 @@
|
||||
@change="importFromJSON"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
@@ -97,7 +97,7 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto rounded-t bg-primary"
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||
>
|
||||
<WorkspaceCurrent :section="t('tab.environments')" />
|
||||
<tippy
|
||||
v-if="environmentType.type === 'my-environments'"
|
||||
interactive
|
||||
@@ -15,14 +16,14 @@
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
selectedEnv.type === 'MY_ENV' && selectedEnv.index !== undefined
|
||||
"
|
||||
:label="myEnvironments[selectedEnv.index].name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('environment.select')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
@@ -36,7 +37,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type !== 'MY_ENV'
|
||||
@@ -52,7 +53,7 @@
|
||||
"
|
||||
/>
|
||||
<hr v-if="myEnvironments.length > 0" />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
:label="gen.name"
|
||||
@@ -74,12 +75,12 @@
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="selectedEnv.name"
|
||||
:label="selectedEnv.name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('environment.select')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
@@ -92,7 +93,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type !== 'TEAM_ENV'
|
||||
@@ -111,7 +112,7 @@
|
||||
v-if="loading"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<hr v-if="teamEnvironmentList.length > 0" />
|
||||
@@ -119,7 +120,7 @@
|
||||
v-if="environmentType.selectedTeam !== undefined"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
:label="gen.environment.name"
|
||||
@@ -144,7 +145,7 @@
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,11 +157,6 @@
|
||||
class="border-b border-dividerLight"
|
||||
@edit-environment="editEnvironment('Global')"
|
||||
/>
|
||||
<EnvironmentsChooseType
|
||||
:environment-type="environmentType"
|
||||
@update-environment-type="updateEnvironmentType"
|
||||
@update-selected-team="updateSelectedTeam"
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsTeams
|
||||
@@ -184,7 +180,7 @@
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { platform } from "~/platform"
|
||||
import { Team } from "~/helpers/backend/graphql"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import {
|
||||
@@ -198,12 +194,16 @@ import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type EnvironmentType = "my-environments" | "team-environments"
|
||||
|
||||
type SelectedTeam = Team | undefined
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
type EnvironmentsChooseType = {
|
||||
type: EnvironmentType
|
||||
@@ -227,12 +227,11 @@ const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||
environmentType.value.selectedTeam = newSelectedTeam
|
||||
}
|
||||
const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
|
||||
environmentType.value.type = newEnvironmentType
|
||||
}
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
const teamListFetched = ref(false)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
|
||||
const adapter = new TeamEnvironmentAdapter(undefined)
|
||||
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
||||
@@ -244,9 +243,64 @@ const loading = computed(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => environmentType.value.selectedTeam?.id,
|
||||
(newTeamID) => {
|
||||
adapter.changeTeamID(newTeamID)
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
if (newTeams && !teamListFetched.value) {
|
||||
teamListFetched.value = true
|
||||
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
|
||||
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||
if (team) updateSelectedTeam(team)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const switchToMyEnvironments = () => {
|
||||
environmentType.value.selectedTeam = undefined
|
||||
updateEnvironmentType("my-environments")
|
||||
adapter.changeTeamID(undefined)
|
||||
}
|
||||
|
||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||
if (newSelectedTeam) {
|
||||
environmentType.value.selectedTeam = newSelectedTeam
|
||||
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
|
||||
updateEnvironmentType("team-environments")
|
||||
}
|
||||
}
|
||||
const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
|
||||
environmentType.value.type = newEnvironmentType
|
||||
}
|
||||
|
||||
watch(
|
||||
() => environmentType.value.selectedTeam,
|
||||
(newTeam) => {
|
||||
if (newTeam) {
|
||||
adapter.changeTeamID(newTeam.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
|
||||
// Used to switch environment type and team when user switch workspace in the global workspace switcher
|
||||
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team
|
||||
// If there is no teamID, switch to my environment
|
||||
watch(
|
||||
() => workspace.value.teamID,
|
||||
(teamID) => {
|
||||
if (!teamID) {
|
||||
switchToMyEnvironments()
|
||||
} else if (teamID) {
|
||||
const team = myTeams.value?.find((t) => t.id === teamID)
|
||||
if (team) {
|
||||
updateSelectedTeam(team)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -254,7 +308,7 @@ watch(
|
||||
() => currentUser.value,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
updateEnvironmentType("my-environments")
|
||||
switchToMyEnvironments()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t(`environment.${action}`)"
|
||||
@@ -28,13 +28,13 @@
|
||||
{{ t("environment.variable_list") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="clearIcon"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlus"
|
||||
:title="t('add.new')"
|
||||
@@ -69,7 +69,7 @@
|
||||
:name="'value' + index"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
@@ -92,7 +92,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@@ -104,12 +104,12 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveEnvironment"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -117,7 +117,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -147,6 +147,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { environmentsStore } from "~/newstore/environments"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
@@ -304,8 +305,7 @@ const saveEnvironment = () => {
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(name.value)
|
||||
updateEnvironment(envList.value.length - 1, environmentUpdated)
|
||||
createEnvironment(name.value, environmentUpdated.variables)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -316,8 +316,21 @@ const saveEnvironment = () => {
|
||||
setGlobalEnvVariables(environmentUpdated.variables)
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else if (props.editingEnvironmentIndex !== null) {
|
||||
const envID =
|
||||
environmentsStore.value.environments[props.editingEnvironmentIndex].id
|
||||
|
||||
// Editing an environment
|
||||
updateEnvironment(props.editingEnvironmentIndex, environmentUpdated)
|
||||
updateEnvironment(
|
||||
props.editingEnvironmentIndex,
|
||||
envID
|
||||
? {
|
||||
...environmentUpdated,
|
||||
id: envID,
|
||||
}
|
||||
: {
|
||||
...environmentUpdated,
|
||||
}
|
||||
)
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -53,7 +53,7 @@
|
||||
"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('action.edit')}`"
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-if="environmentIndex !== 'Global'"
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
@@ -94,7 +94,7 @@
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_environment')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@@ -115,14 +115,12 @@ import {
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
createEnvironment,
|
||||
setEnvironmentVariables,
|
||||
getGlobalVariables,
|
||||
environmentsStore,
|
||||
} from "~/newstore/environments"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -140,23 +138,23 @@ const confirmRemove = ref(false)
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof SmartItem>()
|
||||
const duplicate = ref<typeof SmartItem>()
|
||||
const deleteAction = ref<typeof SmartItem>()
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
if (props.environmentIndex === null) return
|
||||
if (props.environmentIndex !== "Global")
|
||||
deleteEnvironment(props.environmentIndex)
|
||||
if (props.environmentIndex !== "Global") {
|
||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||
}
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
const duplicateEnvironments = () => {
|
||||
if (props.environmentIndex === null) return
|
||||
if (props.environmentIndex === "Global") {
|
||||
createEnvironment(`Global - ${t("action.duplicate")}`)
|
||||
setEnvironmentVariables(
|
||||
environmentsStore.value.environments.length - 1,
|
||||
createEnvironment(
|
||||
`Global - ${t("action.duplicate")}`,
|
||||
cloneDeep(getGlobalVariables())
|
||||
)
|
||||
} else duplicateEnvironment(props.environmentIndex)
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="`${t('action.new')}`"
|
||||
class="!rounded-none"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
:title="t('modal.import_export')"
|
||||
@@ -45,7 +45,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t(`environment.${action}`)"
|
||||
@@ -29,13 +29,13 @@
|
||||
{{ t("environment.variable_list") }}
|
||||
</label>
|
||||
<div v-if="!isViewer" class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="clearIcon"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlus"
|
||||
:title="t('add.new')"
|
||||
@@ -73,7 +73,7 @@
|
||||
:readonly="isViewer"
|
||||
/>
|
||||
<div v-if="!isViewer" class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
@@ -96,14 +96,14 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@@ -116,13 +116,13 @@
|
||||
</template>
|
||||
<template v-if="!isViewer" #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
:loading="isLoading"
|
||||
outline
|
||||
@click="saveEnvironment"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -130,7 +130,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
@@ -42,7 +42,7 @@
|
||||
@keyup.delete="deleteAction!.$el.click()"
|
||||
@keyup.escape="options!.tippy().hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('action.edit')}`"
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -66,7 +66,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -82,7 +82,7 @@
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_environment')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@@ -108,7 +108,7 @@ import IconCopy from "~icons/lucide/copy"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -126,9 +126,9 @@ const confirmRemove = ref(false)
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof SmartItem>()
|
||||
const duplicate = ref<typeof SmartItem>()
|
||||
const deleteAction = ref<typeof SmartItem>()
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
pipe(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
@@ -12,7 +12,7 @@
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="`${t('action.new')}`"
|
||||
@@ -20,21 +20,21 @@
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="team !== undefined && team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
:icon="IconArchive"
|
||||
:title="t('modal.import_export')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
@@ -56,7 +56,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
@@ -66,7 +66,7 @@
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@@ -87,14 +87,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||
<SmartSpinner class="my-4" />
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
<EnvironmentsTeamsDetails
|
||||
@@ -125,14 +125,14 @@ import { useColorMode } from "~/composables/theming"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { Team } from "~/helpers/backend/graphql"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
type SelectedTeam = Team | undefined
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
const props = defineProps<{
|
||||
team: SelectedTeam
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('auth.login_to_hoppscotch')}`"
|
||||
@@ -8,25 +8,25 @@
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:loading="signingInWithGitHub"
|
||||
:icon="IconGithub"
|
||||
:label="`${t('auth.continue_with_github')}`"
|
||||
@click="signInWithGithub"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:loading="signingInWithGoogle"
|
||||
:icon="IconGoogle"
|
||||
:label="`${t('auth.continue_with_google')}`"
|
||||
@click="signInWithGoogle"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:loading="signingInWithMicrosoft"
|
||||
:icon="IconMicrosoft"
|
||||
:label="`${t('auth.continue_with_microsoft')}`"
|
||||
@click="signInWithMicrosoft"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconEmail"
|
||||
:label="`${t('auth.continue_with_email')}`"
|
||||
@click="mode = 'email'"
|
||||
@@ -55,7 +55,7 @@
|
||||
{{ t("auth.email") }}
|
||||
</label>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:label="`${t('auth.send_magic_link')}`"
|
||||
@@ -76,24 +76,27 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div v-if="mode === 'sign-in'" class="text-secondaryLight text-tiny">
|
||||
<div
|
||||
v-if="mode === 'sign-in' && tosLink && privacyPolicyLink"
|
||||
class="text-secondaryLight text-tiny"
|
||||
>
|
||||
By signing in, you are agreeing to our
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
to="https://docs.hoppscotch.io/terms"
|
||||
:to="tosLink"
|
||||
blank
|
||||
label="Terms of Service"
|
||||
/>
|
||||
and
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
to="https://docs.hoppscotch.io/privacy"
|
||||
:to="privacyPolicyLink"
|
||||
blank
|
||||
label="Privacy Policy"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mode === 'email'">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('auth.all_sign_in_options')"
|
||||
:icon="IconArrowLeft"
|
||||
class="!p-0"
|
||||
@@ -104,20 +107,20 @@
|
||||
v-if="mode === 'email-sent'"
|
||||
class="flex justify-between flex-1 text-secondaryLight"
|
||||
>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
:label="t('auth.re_enter_email')"
|
||||
:icon="IconArrowLeft"
|
||||
@click="mode = 'email'"
|
||||
/>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
:label="`${t('action.dismiss')}`"
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -141,6 +144,9 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK
|
||||
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
|
||||
|
||||
return {
|
||||
subscribeToStream,
|
||||
t: useI18n(),
|
||||
@@ -150,6 +156,8 @@ export default defineComponent({
|
||||
IconEmail,
|
||||
IconMicrosoft,
|
||||
IconArrowLeft,
|
||||
tosLink,
|
||||
privacyPolicyLink,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex" @click="OpenLogoutModal()">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="logoutItem"
|
||||
:icon="IconLogOut"
|
||||
:label="`${t('auth.logout')}`"
|
||||
@@ -8,7 +8,7 @@
|
||||
:shortcut="shortcut"
|
||||
@click="OpenLogoutModal()"
|
||||
/>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmLogout"
|
||||
:title="`${t('confirm.logout')}`"
|
||||
@hide-modal="confirmLogout = false"
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary class="pr-8 ml-2 rounded-none" :label="authName" />
|
||||
<HoppButtonSecondary
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
:label="authName"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
@@ -23,7 +26,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@@ -34,7 +37,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Basic Auth'"
|
||||
@@ -45,7 +48,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="Bearer Token"
|
||||
:icon="authName === 'Bearer' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Bearer'"
|
||||
@@ -56,7 +59,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="OAuth 2.0"
|
||||
:icon="authName === 'OAuth 2.0' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@@ -67,7 +70,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="API key"
|
||||
:icon="authName === 'API key' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'API key'"
|
||||
@@ -83,27 +86,27 @@
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<!-- <SmartCheckbox
|
||||
<!-- <HoppSmartCheckbox
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ t("authorization.include_in_url") }}
|
||||
</SmartCheckbox> -->
|
||||
<SmartCheckbox
|
||||
</HoppSmartCheckbox> -->
|
||||
<HoppSmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>
|
||||
{{ t("state.enabled") }}
|
||||
</SmartCheckbox>
|
||||
<ButtonSecondary
|
||||
</HoppSmartCheckbox>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@@ -124,7 +127,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.authorization") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
@@ -180,7 +183,7 @@
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="addTo || t('state.none')"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
@@ -192,7 +195,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||
:active="addTo === 'Headers'"
|
||||
:label="'Headers'"
|
||||
@@ -203,7 +206,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="
|
||||
addTo === 'Query params' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -229,7 +232,7 @@
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
:label="t('authorization.learn')"
|
||||
:icon="IconExternalLink"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:disabled="connected"
|
||||
@keyup.enter="onConnectClick"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { platform } from "~/platform"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
@@ -48,7 +48,7 @@ const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-full">
|
||||
<SmartTabs
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
<HoppSmartTab
|
||||
:id="'query'"
|
||||
:label="`${t('tab.query')}`"
|
||||
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
|
||||
@@ -17,7 +17,7 @@
|
||||
{{ t("request.query") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.run'
|
||||
@@ -27,7 +27,7 @@
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="runQuery()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
@@ -37,33 +37,33 @@
|
||||
class="rounded-none"
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/graphql"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLQuery()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabledQuery }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabledQuery = !linewrapEnabledQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyQueryIcon"
|
||||
@click="prettifyQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyQueryIcon"
|
||||
@@ -72,8 +72,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div ref="queryEditor" class="flex flex-col flex-1"></div>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'variables'"
|
||||
:label="`${t('tab.variables')}`"
|
||||
:indicator="variableString && variableString.length > 0 ? true : false"
|
||||
@@ -85,20 +85,20 @@
|
||||
{{ t("request.variables") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/graphql"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLVariables()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabledVariable }"
|
||||
@@ -107,13 +107,13 @@
|
||||
linewrapEnabledVariable = !linewrapEnabledVariable
|
||||
"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyVariablesIcon"
|
||||
@click="prettifyVariableString"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyVariablesIcon"
|
||||
@@ -122,8 +122,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div ref="variableEditor" class="flex flex-col flex-1"></div>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
|
||||
@@ -135,34 +135,34 @@
|
||||
{{ t("tab.headers") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/graphql"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@@ -192,7 +192,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
@@ -210,7 +210,7 @@
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<SmartAutoComplete
|
||||
<HoppSmartAutoComplete
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
@@ -250,7 +250,7 @@
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
@@ -278,7 +278,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
@@ -302,7 +302,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.headers") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
@@ -311,11 +311,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<GraphqlAuthorization />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<CollectionsSaveRequest
|
||||
mode="graphql"
|
||||
:show="showSaveRequestModal"
|
||||
@@ -354,7 +354,7 @@ import {
|
||||
parseRawKeyValueEntriesE,
|
||||
RawKeyValueEntry,
|
||||
} from "@hoppscotch/data"
|
||||
import draggable from "vuedraggable"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { clone, cloneDeep, isEqual } from "lodash-es"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
@@ -379,7 +379,7 @@ import {
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { platform } from "~/platform"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
@@ -748,7 +748,7 @@ const runQuery = async () => {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-if="responseString === 'loading'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-else-if="responseString" class="flex flex-col flex-1">
|
||||
@@ -15,14 +15,14 @@
|
||||
{{ t("response.title") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'action.download_file'
|
||||
@@ -30,7 +30,7 @@
|
||||
:icon="downloadResponseIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'action.copy'
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<template>
|
||||
<SmartTabs
|
||||
<HoppSmartTabs
|
||||
v-model="selectedNavigationTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary z-10 top-0"
|
||||
vertical
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab :id="'history'" :icon="IconClock" :label="`${t('tab.history')}`">
|
||||
<HoppSmartTab
|
||||
:id="'history'"
|
||||
:icon="IconClock"
|
||||
:label="`${t('tab.history')}`"
|
||||
>
|
||||
<History :page="'graphql'" @use-history="handleUseHistory" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'collections'"
|
||||
:icon="IconFolder"
|
||||
:label="`${t('tab.collections')}`"
|
||||
>
|
||||
<CollectionsGraphql />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'docs'"
|
||||
:icon="IconBookOpen"
|
||||
:label="`${t('tab.documentation')}`"
|
||||
@@ -51,7 +55,7 @@
|
||||
class="flex flex-1 p-4 py-2 bg-transparent"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
@@ -60,12 +64,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartTabs
|
||||
<HoppSmartTabs
|
||||
v-model="selectedGqlTab"
|
||||
styles="border-t border-b border-dividerLight bg-primary sticky overflow-x-auto flex-shrink-0 z-10 top-sidebarPrimaryStickyFold"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
<HoppSmartTab
|
||||
v-if="queryFields.length > 0"
|
||||
:id="'queries'"
|
||||
:label="`${t('tab.queries')}`"
|
||||
@@ -78,8 +82,8 @@
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="mutationFields.length > 0"
|
||||
:id="'mutations'"
|
||||
:label="`${t('graphql.mutations')}`"
|
||||
@@ -92,8 +96,8 @@
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="subscriptionFields.length > 0"
|
||||
:id="'subscriptions'"
|
||||
:label="`${t('graphql.subscriptions')}`"
|
||||
@@ -106,8 +110,8 @@
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="graphqlTypes.length > 0"
|
||||
:id="'types'"
|
||||
:label="`${t('tab.types')}`"
|
||||
@@ -122,11 +126,11 @@
|
||||
:highlighted-fields="getGqlTypeHighlightedFields(type)"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
/>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'schema'" :icon="IconBox" :label="`${t('tab.schema')}`">
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'schema'" :icon="IconBox" :label="`${t('tab.schema')}`">
|
||||
<div
|
||||
v-if="schemaString"
|
||||
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"
|
||||
@@ -135,27 +139,27 @@
|
||||
{{ t("graphql.schema") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadSchemaIcon"
|
||||
@click="downloadSchema"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copySchemaIcon"
|
||||
@@ -182,8 +186,8 @@
|
||||
{{ t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{{ entry.request.url }}
|
||||
</span>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@@ -24,14 +24,14 @@
|
||||
data-testid="delete_history_entry"
|
||||
@click="emit('delete-entry')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="expand ? t('hide.more') : t('show.more')"
|
||||
:icon="expand ? IconMinimize2 : IconMaximize2"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="expand = !expand"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="!entry.star ? t('add.star') : t('remove.star')"
|
||||
:icon="entry.star ? IconStarOff : IconStar"
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||
>
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex flex-1 p-4 py-2 bg-transparent"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
<WorkspaceCurrent :section="t('tab.history')" />
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/history"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex flex-1 p-4 py-2 bg-transparent"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<ButtonSecondary
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.filter')"
|
||||
:icon="IconFilter"
|
||||
to="https://docs.hoppscotch.io/features/history"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div ref="tippyActions" class="flex flex-col focus:outline-none">
|
||||
<div class="pb-2 pl-4 text-tiny text-secondaryLight">
|
||||
{{ t("action.filter") }}
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.filter')"
|
||||
:icon="IconFilter"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div ref="tippyActions" class="flex flex-col focus:outline-none">
|
||||
<div class="pb-2 pl-4 text-tiny text-secondaryLight">
|
||||
{{ t("action.filter") }}
|
||||
</div>
|
||||
<HoppSmartRadioGroup
|
||||
v-model="filterSelection"
|
||||
:radios="filters"
|
||||
@update:model-value="hide()"
|
||||
/>
|
||||
<hr />
|
||||
<div class="pb-2 pl-4 text-tiny text-secondaryLight">
|
||||
{{ t("action.group_by") }}
|
||||
</div>
|
||||
<HoppSmartRadioGroup
|
||||
v-model="groupSelection"
|
||||
:radios="groups"
|
||||
@update:model-value="hide()"
|
||||
/>
|
||||
</div>
|
||||
<SmartRadioGroup
|
||||
v-model="filterSelection"
|
||||
:radios="filters"
|
||||
@update:model-value="hide()"
|
||||
/>
|
||||
<hr />
|
||||
<div class="pb-2 pl-4 text-tiny text-secondaryLight">
|
||||
{{ t("action.group_by") }}
|
||||
</div>
|
||||
<SmartRadioGroup
|
||||
v-model="groupSelection"
|
||||
:radios="groups"
|
||||
@update:model-value="hide()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
data-testid="clear_history"
|
||||
:disabled="history.length === 0"
|
||||
:icon="IconTrash2"
|
||||
:title="t('action.clear_all')"
|
||||
@click="confirmRemove = true"
|
||||
/>
|
||||
</template>
|
||||
</tippy>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
data-testid="clear_history"
|
||||
:disabled="history.length === 0"
|
||||
:icon="IconTrash2"
|
||||
:title="t('action.clear_all')"
|
||||
@click="confirmRemove = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
@@ -81,7 +84,7 @@
|
||||
{{ filteredHistoryGroupIndex }}
|
||||
</span>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@@ -128,7 +131,7 @@
|
||||
<span class="mt-2 mb-4 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText || filterSelection }}"
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear')"
|
||||
outline
|
||||
@click="
|
||||
@@ -139,23 +142,12 @@
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_history')}`"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="clearHistory"
|
||||
/>
|
||||
<HttpReqChangeConfirmModal
|
||||
:show="confirmChange"
|
||||
@hide-modal="confirmChange = false"
|
||||
@save-change="saveRequestChange"
|
||||
@discard-change="discardRequestChange"
|
||||
/>
|
||||
<CollectionsSaveRequest
|
||||
mode="rest"
|
||||
:show="showSaveRequestModal"
|
||||
@hide-modal="showSaveRequestModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -166,17 +158,11 @@ import IconTrash from "~icons/lucide/trash"
|
||||
import IconFilter from "~icons/lucide/filter"
|
||||
import { computed, ref, Ref, toRaw } from "vue"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import {
|
||||
HoppGQLRequest,
|
||||
HoppRESTRequest,
|
||||
isEqualHoppRESTRequest,
|
||||
safelyExtractRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { groupBy, escapeRegExp, filter } from "lodash-es"
|
||||
import { useTimeAgo } from "@vueuse/core"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -192,20 +178,10 @@ import {
|
||||
RESTHistoryEntry,
|
||||
GQLHistoryEntry,
|
||||
} from "~/newstore/history"
|
||||
import {
|
||||
getDefaultRESTRequest,
|
||||
getRESTRequest,
|
||||
getRESTSaveContext,
|
||||
setRESTRequest,
|
||||
setRESTSaveContext,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
|
||||
import HistoryRestCard from "./rest/Card.vue"
|
||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||
import { createNewTab } from "~/helpers/rest/tab"
|
||||
|
||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||
|
||||
@@ -226,10 +202,6 @@ const filterText = ref("")
|
||||
const showMore = ref(false)
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const clickedHistory = ref<HistoryEntry | null>(null)
|
||||
const confirmChange = ref(false)
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
|
||||
props.page === "rest" ? restHistory$ : graphqlHistory$,
|
||||
[]
|
||||
@@ -284,7 +256,7 @@ const filters = computed(() => [
|
||||
{ value: "STARRED" as const, label: t("filter.starred") },
|
||||
])
|
||||
|
||||
type FilterMode = typeof filters["value"][number]["value"]
|
||||
type FilterMode = (typeof filters)["value"][number]["value"]
|
||||
|
||||
const filterSelection = ref<FilterMode>("ALL")
|
||||
|
||||
@@ -293,7 +265,7 @@ const groups = computed(() => [
|
||||
{ value: "URL" as const, label: t("group.url") },
|
||||
])
|
||||
|
||||
type GroupMode = typeof groups["value"][number]["value"]
|
||||
type GroupMode = (typeof groups)["value"][number]["value"]
|
||||
|
||||
const groupSelection = ref<GroupMode>("TIME")
|
||||
|
||||
@@ -323,111 +295,13 @@ const clearHistory = () => {
|
||||
toast.success(`${t("state.history_deleted")}`)
|
||||
}
|
||||
|
||||
const setRestReq = (request: HoppRESTRequest | null | undefined) => {
|
||||
setRESTRequest(safelyExtractRESTRequest(request, getDefaultRESTRequest()))
|
||||
}
|
||||
|
||||
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
|
||||
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
||||
const useHistory = (entry: RESTHistoryEntry) => {
|
||||
const currentFullReq = getRESTRequest()
|
||||
|
||||
const currentReqWithNoChange = getRESTSaveContext()?.req
|
||||
|
||||
// checks if the current request is the same as the save context request if present
|
||||
if (
|
||||
currentReqWithNoChange &&
|
||||
isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)
|
||||
) {
|
||||
props.page === "rest" && setRestReq(entry.request)
|
||||
clickedHistory.value = entry
|
||||
}
|
||||
// Initial state trigers a popup
|
||||
else if (!clickedHistory.value) {
|
||||
clickedHistory.value = entry
|
||||
confirmChange.value = true
|
||||
return
|
||||
}
|
||||
// Checks if there are any change done in current request and the history request
|
||||
else if (
|
||||
!isEqualHoppRESTRequest(
|
||||
currentFullReq,
|
||||
clickedHistory.value.request as HoppRESTRequest
|
||||
)
|
||||
) {
|
||||
clickedHistory.value = entry
|
||||
confirmChange.value = true
|
||||
} else {
|
||||
props.page === "rest" && setRestReq(entry.request)
|
||||
clickedHistory.value = entry
|
||||
}
|
||||
}
|
||||
|
||||
/** Save current request to the collection */
|
||||
const saveRequestChange = () => {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
saveCurrentRequest(saveCtx)
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
/** Discard changes and change the current request and remove the collection context */
|
||||
const discardRequestChange = () => {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
if (saveCtx) {
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
clickedHistory.value &&
|
||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
||||
if (!saveCtx) {
|
||||
showSaveRequestModal.value = true
|
||||
return
|
||||
}
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
try {
|
||||
editRESTRequest(
|
||||
saveCtx.folderPath,
|
||||
saveCtx.requestIndex,
|
||||
getRESTRequest()
|
||||
)
|
||||
clickedHistory.value &&
|
||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
||||
setRESTSaveContext(null)
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setRESTSaveContext(null)
|
||||
saveCurrentRequest(null)
|
||||
}
|
||||
} else if (saveCtx.originLocation === "team-collection") {
|
||||
const req = getRESTRequest()
|
||||
try {
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
title: req.name,
|
||||
request: JSON.stringify(req),
|
||||
},
|
||||
})().then((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
} else {
|
||||
toast.success(`${t("request.saved")}`)
|
||||
}
|
||||
})
|
||||
clickedHistory.value &&
|
||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
||||
setRESTSaveContext(null)
|
||||
} catch (error) {
|
||||
showSaveRequestModal.value = true
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(error)
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
}
|
||||
createNewTab({
|
||||
request: entry.request,
|
||||
isDirty: false,
|
||||
})
|
||||
}
|
||||
|
||||
const isRESTHistoryEntry = (
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{{ entry.request.endpoint }}
|
||||
</span>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@@ -35,7 +35,7 @@
|
||||
data-testid="delete_history_entry"
|
||||
@click="emit('delete-entry')"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="!entry.star ? t('add.star') : t('remove.star')"
|
||||
:class="{ 'group-hover:inline-flex hidden': !entry.star }"
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary class="pr-8 ml-2 rounded-none" :label="authName" />
|
||||
<HoppButtonSecondary
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
:label="authName"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
@@ -23,57 +26,57 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'none'
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Basic Auth'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'basic'
|
||||
auth.authType = 'basic'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="Bearer Token"
|
||||
:icon="authName === 'Bearer' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Bearer'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'bearer'
|
||||
auth.authType = 'bearer'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="OAuth 2.0"
|
||||
:icon="authName === 'OAuth 2.0' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'oauth-2'
|
||||
auth.authType = 'oauth-2'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
label="API key"
|
||||
:icon="authName === 'API key' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'API key'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'api-key'
|
||||
auth.authType = 'api-key'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -83,26 +86,26 @@
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<!-- <SmartCheckbox
|
||||
<!-- <HoppSmartCheckbox
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</SmartCheckbox>-->
|
||||
<SmartCheckbox
|
||||
</HoppSmartCheckbox>-->
|
||||
<HoppSmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>{{ t("state.enabled") }}</SmartCheckbox
|
||||
>{{ t("state.enabled") }}</HoppSmartCheckbox
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@@ -111,7 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'none'"
|
||||
v-if="auth.authType === 'none'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
@@ -121,7 +124,7 @@
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.authorization") }}</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
@@ -133,91 +136,22 @@
|
||||
</div>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="authType === 'basic'">
|
||||
<div v-if="auth.authType === 'basic'">
|
||||
<HttpAuthorizationBasic v-model="auth" />
|
||||
</div>
|
||||
<div v-if="auth.authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicUsername"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicPassword"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'">
|
||||
<div v-if="auth.authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||
</div>
|
||||
<HttpOAuth2Authorization v-model="auth" />
|
||||
</div>
|
||||
<div v-if="authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
</div>
|
||||
<div v-if="authType === 'api-key'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiKey" placeholder="Key" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiValue" placeholder="Value" />
|
||||
</div>
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
<label class="ml-4 text-secondaryLight">
|
||||
{{ t("authorization.pass_key_by") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
:label="addTo || t('state.none')"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||
:active="addTo === 'Headers'"
|
||||
:label="'Headers'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Headers'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
:icon="
|
||||
addTo === 'Query params' ? IconCircleDot : IconCircle
|
||||
"
|
||||
:active="addTo === 'Query params'"
|
||||
:label="'Query params'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Query params'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'api-key'">
|
||||
<HttpAuthorizationApiKey v-model="auth" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -226,7 +160,7 @@
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
:label="t('authorization.learn')"
|
||||
:icon="IconExternalLink"
|
||||
@@ -245,49 +179,40 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import { computed, ref, Ref } from "vue"
|
||||
import {
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTAuthAPIKey,
|
||||
} from "@hoppscotch/data"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppRESTAuth } from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
const props = defineProps<{
|
||||
modelValue: HoppRESTAuth
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTAuth): 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(() => {
|
||||
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 authName = computed(() =>
|
||||
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
||||
)
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
const basicUsername = pluckRef(auth as Ref<HoppRESTAuthBasic>, "username")
|
||||
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
|
||||
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
|
||||
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
|
||||
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
|
||||
if (typeof addTo.value === "undefined") {
|
||||
addTo.value = "Headers"
|
||||
apiKey.value = ""
|
||||
apiValue.value = ""
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
@@ -298,5 +223,4 @@ const clearContent = () => {
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const authTippyActions = ref<any | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
:label="contentType || t('state.none')"
|
||||
<HoppButtonSecondary
|
||||
:label="body.contentType || t('state.none')"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
@@ -26,13 +26,13 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:label="t('state.none')"
|
||||
:info-icon="contentType === null ? IconDone : null"
|
||||
:active-info-icon="contentType === null"
|
||||
:info-icon="(body.contentType === null ? IconDone : null) as any"
|
||||
:active-info-icon="body.contentType === null"
|
||||
@click="
|
||||
() => {
|
||||
contentType = null
|
||||
body.contentType = null
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -50,19 +50,19 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="(
|
||||
contentTypeItem, contentTypeIndex
|
||||
) in contentTypeItems.contentTypes"
|
||||
:key="`contentTypeItem-${contentTypeIndex}`"
|
||||
:label="contentTypeItem"
|
||||
:info-icon="
|
||||
contentTypeItem === contentType ? IconDone : null
|
||||
contentTypeItem === body.contentType ? IconDone : null
|
||||
"
|
||||
:active-info-icon="contentTypeItem === contentType"
|
||||
:active-info-icon="contentTypeItem === body.contentType"
|
||||
@click="
|
||||
() => {
|
||||
contentType = contentTypeItem
|
||||
body.contentType = contentTypeItem
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('request.override_help')"
|
||||
:label="
|
||||
@@ -93,13 +93,17 @@
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<HttpBodyParameters v-if="contentType === 'multipart/form-data'" />
|
||||
<HttpURLEncodedParams
|
||||
v-else-if="contentType === 'application/x-www-form-urlencoded'"
|
||||
<HttpBodyParameters
|
||||
v-if="body.contentType === 'multipart/form-data'"
|
||||
v-model="body"
|
||||
/>
|
||||
<HttpRawBody v-else-if="contentType !== null" :content-type="contentType" />
|
||||
<HttpURLEncodedParams
|
||||
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
||||
v-model="body"
|
||||
/>
|
||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||
<div
|
||||
v-if="contentType == null"
|
||||
v-if="body.contentType == null"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
@@ -109,7 +113,7 @@
|
||||
:alt="`${t('empty.body')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.body") }}</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="`${t('app.documentation')}`"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
@@ -123,38 +127,37 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import { computed, ref } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { HoppRESTHeader, HoppRESTReqBody } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { computed, ref } from "vue"
|
||||
import { segmentedContentTypes } from "~/helpers/utils/contenttypes"
|
||||
import {
|
||||
restContentType$,
|
||||
restHeaders$,
|
||||
setRESTContentType,
|
||||
setRESTHeaders,
|
||||
addRESTHeader,
|
||||
} from "~/newstore/RESTSession"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change-tab", value: string): void
|
||||
const props = defineProps<{
|
||||
body: HoppRESTReqBody
|
||||
headers: HoppRESTHeader[]
|
||||
}>()
|
||||
|
||||
const contentType = useStream(restContentType$, null, setRESTContentType)
|
||||
const emit = defineEmits<{
|
||||
(e: "change-tab", value: RequestOptionTabs): void
|
||||
(e: "update:headers", value: HoppRESTHeader[]): void
|
||||
(e: "update:body", value: HoppRESTReqBody): void
|
||||
}>()
|
||||
|
||||
// The functional headers list (the headers actually in the system)
|
||||
const headers = useStream(restHeaders$, [], setRESTHeaders)
|
||||
const headers = useVModel(props, "headers", emit)
|
||||
const body = useVModel(props, "body", emit)
|
||||
|
||||
const overridenContentType = computed(() =>
|
||||
pipe(
|
||||
@@ -168,7 +171,9 @@ const overridenContentType = computed(() =>
|
||||
const contentTypeOverride = (tab: RequestOptionTabs) => {
|
||||
emit("change-tab", tab)
|
||||
if (!isContentTypeAlreadyExist()) {
|
||||
addRESTHeader({
|
||||
// TODO: Fix this
|
||||
|
||||
headers.value.push({
|
||||
key: "Content-Type",
|
||||
value: "",
|
||||
active: true,
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("request.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@@ -44,7 +44,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
@@ -76,10 +76,10 @@
|
||||
/>
|
||||
<div v-if="entry.isFile" class="file-chips-container">
|
||||
<div class="space-x-2 file-chips-wrapper">
|
||||
<SmartFileChip
|
||||
<HoppSmartFileChip
|
||||
v-for="(file, fileIndex) in entry.value"
|
||||
:key="`param-${index}-file-${fileIndex}`"
|
||||
>{{ file.name }}</SmartFileChip
|
||||
>{{ file.name }}</HoppSmartFileChip
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@
|
||||
</label>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
entry.hasOwnProperty('active')
|
||||
@@ -140,7 +140,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
@@ -163,7 +163,7 @@
|
||||
:alt="`${t('empty.body')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.body") }}</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
@@ -186,14 +186,26 @@ import { ref, watch } from "vue"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { FormDataKeyValue } from "@hoppscotch/data"
|
||||
import { FormDataKeyValue, HoppRESTReqBody } from "@hoppscotch/data"
|
||||
import { isEqual, clone } from "lodash-es"
|
||||
import draggable from "vuedraggable"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Body
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: Body): void
|
||||
}>()
|
||||
|
||||
const body = useVModel(props, "modelValue", emit)
|
||||
|
||||
type WorkingFormDataKeyValue = { id: number; entry: FormDataKeyValue }
|
||||
|
||||
@@ -206,7 +218,7 @@ const idTicker = ref(0)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body")
|
||||
const bodyParams = pluckRef(body, "body")
|
||||
|
||||
// The UI representation of the parameters list (has the empty end param)
|
||||
const workingParams = ref<WorkingFormDataKeyValue[]>([
|
||||
@@ -355,7 +367,7 @@ const clearContent = () => {
|
||||
const setRequestAttachment = (
|
||||
index: number,
|
||||
entry: FormDataKeyValue,
|
||||
event: InputEvent
|
||||
event: InputEvent | Event
|
||||
) => {
|
||||
// check if file exists or not
|
||||
if ((event.target as HTMLInputElement).files?.length === 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('request.generate_code')}`"
|
||||
@@ -18,7 +18,7 @@
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="
|
||||
CodegenDefinitions.find((x) => x.name === codegenType).caption
|
||||
"
|
||||
@@ -28,7 +28,7 @@
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="sticky top-0 flex-shrink-0 overflow-x-auto">
|
||||
<div class="sticky z-10 top-0 flex-shrink-0 overflow-x-auto">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
@@ -43,7 +43,7 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="codegen in filteredCodegenDefinitions"
|
||||
:key="codegen.name"
|
||||
:label="codegen.caption"
|
||||
@@ -89,20 +89,20 @@
|
||||
{{ t("request.generated_code") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@@ -119,13 +119,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.copy')}`"
|
||||
:icon="copyCodeIcon"
|
||||
outline
|
||||
@click="copyRequestCode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.dismiss')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -133,7 +133,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -148,7 +148,6 @@ import {
|
||||
resolvesEnvsInBody,
|
||||
} from "~/helpers/utils/EffectiveURL"
|
||||
import { getAggregateEnvs } from "~/newstore/environments"
|
||||
import { getRESTRequest } from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
@@ -164,6 +163,8 @@ import {
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import cloneDeep from "lodash-es/cloneDeep"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -177,7 +178,7 @@ const emit = defineEmits<{
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const request = ref(getRESTRequest())
|
||||
const request = ref(cloneDeep(currentActiveTab.value.document.request))
|
||||
const codegenType = ref<CodegenName>("shell-curl")
|
||||
const errorState = ref(false)
|
||||
|
||||
@@ -246,7 +247,7 @@ watch(
|
||||
() => props.show,
|
||||
(goingToShow) => {
|
||||
if (goingToShow) {
|
||||
request.value = getRESTRequest()
|
||||
request.value = cloneDeep(currentActiveTab.value.document.request)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("request.header_list") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/headers"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
@@ -28,14 +28,14 @@
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@@ -48,7 +48,7 @@
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingHeaders"
|
||||
:item-key="(header) => `header-${header.id}`"
|
||||
:item-key="(header: WorkingHeader) => `header-${header.id}`"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
@@ -61,7 +61,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
@@ -79,7 +79,7 @@
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<SmartAutoComplete
|
||||
<HoppSmartAutoComplete
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
@@ -110,7 +110,7 @@
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
@@ -138,7 +138,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
@@ -165,7 +165,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLock"
|
||||
class="opacity-25 cursor-auto text-secondaryLight bg-divider"
|
||||
tabindex="-1"
|
||||
@@ -182,19 +182,19 @@
|
||||
readonly
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="header.source === 'auth'"
|
||||
:icon="masking ? IconEye : IconEyeOff"
|
||||
@click="toggleMask()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconArrowUpRight"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="IconArrowUpRight"
|
||||
@click="changeTab(header.source)"
|
||||
/>
|
||||
@@ -213,7 +213,7 @@
|
||||
:alt="`${t('empty.headers')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.headers") }}</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
@@ -240,10 +240,11 @@ import IconEyeOff from "~icons/lucide/eye-off"
|
||||
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { computed, reactive, Ref, ref, watch } from "vue"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { isEqual, cloneDeep } from "lodash-es"
|
||||
import {
|
||||
HoppRESTHeader,
|
||||
HoppRESTRequest,
|
||||
parseRawKeyValueEntriesE,
|
||||
rawKeyValueEntriesToString,
|
||||
RawKeyValueEntry,
|
||||
@@ -253,18 +254,12 @@ import * as RA from "fp-ts/ReadonlyArray"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import draggable from "vuedraggable"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import {
|
||||
getRESTRequest,
|
||||
restHeaders$,
|
||||
restRequest$,
|
||||
setRESTHeaders,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
@@ -274,6 +269,7 @@ import {
|
||||
getComputedHeaders,
|
||||
} from "~/helpers/utils/EffectiveURL"
|
||||
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -288,10 +284,16 @@ const linewrapEnabled = ref(true)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = defineProps<{ modelValue: HoppRESTRequest }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change-tab", value: RequestOptionTabs): void
|
||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
useCodemirror(
|
||||
bulkEditor,
|
||||
bulkHeaders,
|
||||
@@ -307,13 +309,10 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
// The functional headers list (the headers actually in the system)
|
||||
const headers = useStream(restHeaders$, [], setRESTHeaders) as Ref<
|
||||
HoppRESTHeader[]
|
||||
>
|
||||
type WorkingHeader = HoppRESTHeader & { id: number }
|
||||
|
||||
// The UI representation of the headers list (has the empty end headers)
|
||||
const workingHeaders = ref<Array<HoppRESTHeader & { id: number }>>([
|
||||
const workingHeaders = ref<Array<WorkingHeader>>([
|
||||
{
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
@@ -339,7 +338,7 @@ watch(workingHeaders, (headersList) => {
|
||||
|
||||
// Sync logic between headers and working/bulk headers
|
||||
watch(
|
||||
headers,
|
||||
request.value.headers,
|
||||
(newHeadersList) => {
|
||||
// Sync should overwrite working headers
|
||||
const filteredWorkingHeaders = pipe(
|
||||
@@ -388,8 +387,8 @@ watch(workingHeaders, (newWorkingHeaders) => {
|
||||
)
|
||||
)
|
||||
|
||||
if (!isEqual(headers.value, fixedHeaders)) {
|
||||
headers.value = cloneDeep(fixedHeaders)
|
||||
if (!isEqual(request.value.headers, fixedHeaders)) {
|
||||
request.value.headers = cloneDeep(fixedHeaders)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -405,8 +404,8 @@ watch(bulkHeaders, (newBulkHeaders) => {
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
|
||||
if (!isEqual(headers.value, filteredBulkHeaders)) {
|
||||
headers.value = filteredBulkHeaders
|
||||
if (!isEqual(props.modelValue, filteredBulkHeaders)) {
|
||||
request.value.headers = filteredBulkHeaders
|
||||
}
|
||||
})
|
||||
|
||||
@@ -481,11 +480,10 @@ const clearContent = () => {
|
||||
bulkHeaders.value = ""
|
||||
}
|
||||
|
||||
const restRequest = useReadonlyStream(restRequest$, getRESTRequest())
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||
|
||||
const computedHeaders = computed(() =>
|
||||
getComputedHeaders(restRequest.value, aggregateEnvs.value).map(
|
||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
||||
(header, index) => ({
|
||||
id: `header-${index}`,
|
||||
...header,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('import.curl')}`"
|
||||
@@ -13,26 +13,26 @@
|
||||
cURL
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@@ -51,13 +51,13 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
ref="importButton"
|
||||
:label="`${t('import.title')}`"
|
||||
outline
|
||||
@click="handleImport"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@@ -65,7 +65,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:icon="pasteIcon"
|
||||
:label="`${t('action.paste')}`"
|
||||
filled
|
||||
@@ -74,14 +74,13 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
|
||||
@@ -94,6 +93,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -144,7 +144,7 @@ const handleImport = () => {
|
||||
try {
|
||||
const req = parseCurlToHoppRESTReq(text)
|
||||
|
||||
setRESTRequest(req)
|
||||
currentActiveTab.value.document.request = req
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`${t("error.curl_invalid_format")}`)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<SmartEnvInput v-model="scope" placeholder="Scope" />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
:label="`${t('authorization.generate_token')}`"
|
||||
@click="handleAccessTokenRequest()"
|
||||
@@ -31,89 +31,72 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref, defineComponent } from "vue"
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
import { tokenRequest } from "~/helpers/oauth"
|
||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
const props = defineProps<{
|
||||
modelValue: HoppRESTAuthOAuth2
|
||||
}>()
|
||||
|
||||
const oidcDiscoveryURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"oidcDiscoveryURL"
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTAuthOAuth2): void
|
||||
}>()
|
||||
|
||||
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
|
||||
const auth = ref(props.modelValue)
|
||||
|
||||
const accessTokenURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"accessTokenURL"
|
||||
)
|
||||
watch(
|
||||
() => auth.value,
|
||||
(val) => {
|
||||
emit("update:modelValue", val)
|
||||
}
|
||||
)
|
||||
|
||||
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
|
||||
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
||||
|
||||
const clientSecret = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"clientSecret"
|
||||
)
|
||||
const authURL = pluckRef(auth, "authURL")
|
||||
|
||||
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
|
||||
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
||||
|
||||
const handleAccessTokenRequest = async () => {
|
||||
if (
|
||||
oidcDiscoveryURL.value === "" &&
|
||||
(authURL.value === "" || accessTokenURL.value === "")
|
||||
) {
|
||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||
return
|
||||
}
|
||||
const envs = getCombinedEnvVariables()
|
||||
const envVars = [...envs.selected, ...envs.global]
|
||||
const clientID = pluckRef(auth, "clientID")
|
||||
|
||||
try {
|
||||
const tokenReqParams = {
|
||||
grantType: "code",
|
||||
oidcDiscoveryUrl: parseTemplateString(
|
||||
oidcDiscoveryURL.value,
|
||||
envVars
|
||||
),
|
||||
authUrl: parseTemplateString(authURL.value, envVars),
|
||||
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
|
||||
clientId: parseTemplateString(clientID.value, envVars),
|
||||
clientSecret: parseTemplateString(clientSecret.value, envVars),
|
||||
scope: parseTemplateString(scope.value, envVars),
|
||||
}
|
||||
await tokenRequest(tokenReqParams)
|
||||
} catch (e) {
|
||||
toast.error(`${e}`)
|
||||
}
|
||||
// TODO: Fix this type error. currently there is no type for clientSecret
|
||||
const clientSecret = pluckRef(auth, "clientSecret" as any)
|
||||
|
||||
const scope = pluckRef(auth, "scope")
|
||||
|
||||
const handleAccessTokenRequest = async () => {
|
||||
if (
|
||||
oidcDiscoveryURL.value === "" &&
|
||||
(authURL.value === "" || accessTokenURL.value === "")
|
||||
) {
|
||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||
return
|
||||
}
|
||||
const envs = getCombinedEnvVariables()
|
||||
const envVars = [...envs.selected, ...envs.global]
|
||||
|
||||
try {
|
||||
const tokenReqParams = {
|
||||
grantType: "code",
|
||||
oidcDiscoveryUrl: parseTemplateString(oidcDiscoveryURL.value, envVars),
|
||||
authUrl: parseTemplateString(authURL.value, envVars),
|
||||
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
|
||||
clientId: parseTemplateString(clientID.value, envVars),
|
||||
clientSecret: parseTemplateString(clientSecret.value, envVars),
|
||||
scope: parseTemplateString(scope.value, envVars),
|
||||
}
|
||||
|
||||
return {
|
||||
oidcDiscoveryURL,
|
||||
authURL,
|
||||
accessTokenURL,
|
||||
clientID,
|
||||
clientSecret,
|
||||
scope,
|
||||
handleAccessTokenRequest,
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
await tokenRequest(tokenReqParams)
|
||||
} catch (e) {
|
||||
toast.error(`${e}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("request.parameter_list") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/parameters"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
@@ -28,14 +28,14 @@
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@@ -61,7 +61,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
@@ -104,7 +104,7 @@
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
param.hasOwnProperty('active')
|
||||
@@ -134,7 +134,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
@@ -157,7 +157,7 @@
|
||||
:alt="`${t('empty.parameters')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
filled
|
||||
@@ -179,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { reactive, Ref, ref, watch } from "vue"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
@@ -192,16 +192,15 @@ import {
|
||||
RawKeyValueEntry,
|
||||
} from "@hoppscotch/data"
|
||||
import { isEqual, cloneDeep } from "lodash-es"
|
||||
import draggable from "vuedraggable"
|
||||
import draggable from "vuedraggable-es"
|
||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
|
||||
import { throwError } from "@functional/error"
|
||||
import { objRemoveKey } from "@functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -232,8 +231,16 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppRESTParam[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: Array<HoppRESTParam>): void
|
||||
}>()
|
||||
|
||||
// The functional parameters list (the parameters actually applied to the session)
|
||||
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
|
||||
const params = useVModel(props, "modelValue", emit)
|
||||
|
||||
// The UI representation of the parameters list (has the empty end param)
|
||||
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("preRequest.javascript_code") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/pre-request-script"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.pre_request_script") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
:label="`${t('preRequest.learn')}`"
|
||||
to="https://docs.hoppscotch.io/features/pre-request-script"
|
||||
blank
|
||||
@@ -66,16 +66,23 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { reactive, ref } from "vue"
|
||||
import { usePreRequestScript } from "~/newstore/RESTSession"
|
||||
import snippets from "@helpers/preRequestScriptSnippets"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import linter from "~/helpers/editor/linting/preRequest"
|
||||
import completer from "~/helpers/editor/completion/preRequest"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const preRequestScript = usePreRequestScript()
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
||||
|
||||
const preRequestEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
@@ -7,27 +7,27 @@
|
||||
{{ t("request.raw_body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
[
|
||||
'application/json',
|
||||
@@ -35,7 +35,7 @@
|
||||
'application/hal+json',
|
||||
'application/vnd.api+json',
|
||||
'application/xml',
|
||||
].includes(contentType)
|
||||
].includes(body.contentType)
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
@@ -43,7 +43,7 @@
|
||||
@click="prettifyRequestBody"
|
||||
/>
|
||||
<label for="payload">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('import.title')"
|
||||
:icon="IconFilePlus"
|
||||
@@ -74,16 +74,14 @@ import IconInfo from "~icons/lucide/info"
|
||||
import { computed, reactive, Ref, ref, watch } from "vue"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { ValidContentTypes } from "@hoppscotch/data"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { HoppRESTReqBody, ValidContentTypes } from "@hoppscotch/data"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { getEditorLangForMimeType } from "@helpers/editorutils"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
||||
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { readFileAsText } from "~/helpers/functional/files"
|
||||
|
||||
@@ -92,27 +90,35 @@ type PossibleContentTypes = Exclude<
|
||||
"multipart/form-data" | "application/x-www-form-urlencoded"
|
||||
>
|
||||
|
||||
type Body = HoppRESTReqBody & { contentType: PossibleContentTypes }
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Body
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: Body): void
|
||||
}>()
|
||||
|
||||
const body = useVModel(props, "modelValue", emit)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const payload = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
contentType: PossibleContentTypes
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
|
||||
const rawParamsBody = pluckRef(body, "body")
|
||||
|
||||
const prettifyIcon = refAutoReset<
|
||||
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand2, 1000)
|
||||
|
||||
const rawInputEditorLang = computed(() =>
|
||||
getEditorLangForMimeType(props.contentType)
|
||||
getEditorLangForMimeType(body.value.contentType)
|
||||
)
|
||||
const langLinter = computed(() =>
|
||||
isJSONContentType(props.contentType) ? jsonLinter : null
|
||||
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
||||
)
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
@@ -175,10 +181,10 @@ const uploadPayload = async (e: Event) => {
|
||||
const prettifyRequestBody = () => {
|
||||
let prettifyBody = ""
|
||||
try {
|
||||
if (props.contentType.endsWith("json")) {
|
||||
if (body.value.contentType.endsWith("json")) {
|
||||
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
||||
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
||||
} else if (props.contentType == "application/xml") {
|
||||
} else if (body.value.contentType == "application/xml") {
|
||||
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||
}
|
||||
rawParamsBody.value = prettifyBody
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.confirm')"
|
||||
@@ -13,28 +13,28 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="t('action.save')"
|
||||
:loading="loading"
|
||||
outline
|
||||
@click="saveChange"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.dont_save')"
|
||||
outline
|
||||
filled
|
||||
@click="discardChange"
|
||||
/>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
<input
|
||||
id="method"
|
||||
class="flex px-4 py-2 font-semibold transition rounded-l cursor-pointer text-secondaryDark w-26 bg-primaryLight"
|
||||
:value="newMethod"
|
||||
:value="tab.document.request.method"
|
||||
:readonly="!isCustomMethod"
|
||||
:placeholder="`${t('request.method')}`"
|
||||
@input="onSelectMethod($event.target.value)"
|
||||
@input="onSelectMethod($event)"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
@@ -30,13 +30,13 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
v-for="(method, index) in methods"
|
||||
:key="`method-${index}`"
|
||||
:label="method"
|
||||
@click="
|
||||
() => {
|
||||
onSelectMethod(method)
|
||||
updateMethod(method)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -50,7 +50,7 @@
|
||||
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-model="newEndpoint"
|
||||
v-model="tab.document.request.endpoint"
|
||||
:placeholder="`${t('request.url')}`"
|
||||
@enter="newSendRequest()"
|
||||
@paste="onPasteUrl($event)"
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 sm:mt-0">
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
id="send"
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t('action.send')} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||
@@ -73,7 +73,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => sendTippyActions.focus()"
|
||||
>
|
||||
<ButtonPrimary
|
||||
<HoppButtonPrimary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.options')"
|
||||
:icon="IconChevronDown"
|
||||
@@ -90,7 +90,7 @@
|
||||
@keyup.delete="clearAll.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="curl"
|
||||
:label="`${t('import.curl')}`"
|
||||
:icon="IconFileCode"
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="show"
|
||||
:label="`${t('show.code')}`"
|
||||
:icon="IconCode2"
|
||||
@@ -114,7 +114,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="clearAll"
|
||||
:label="`${t('action.clear_all')}`"
|
||||
:icon="IconRotateCCW"
|
||||
@@ -130,10 +130,8 @@
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<span
|
||||
class="flex ml-2 transition border rounded border-dividerLight hover:border-dividerDark"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<span class="flex ml-2 transition border rounded border-divider">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
@@ -151,7 +149,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => saveTippyActions.focus()"
|
||||
>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.options')"
|
||||
:icon="IconChevronDown"
|
||||
@@ -163,13 +161,11 @@
|
||||
ref="saveTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.c="copyRequestAction.$el.click()"
|
||||
@keyup.s="saveRequestAction.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<input
|
||||
id="request-name"
|
||||
v-model="requestName"
|
||||
v-model="tab.document.request.name"
|
||||
:placeholder="`${t('request.name')}`"
|
||||
name="request-name"
|
||||
type="text"
|
||||
@@ -177,29 +173,27 @@
|
||||
class="mb-2 input !bg-primaryContrast"
|
||||
@keyup.enter="hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="copyRequestAction"
|
||||
:label="shareButtonText"
|
||||
:icon="copyLinkIcon"
|
||||
:loading="fetchingShareLink"
|
||||
:shortcut="['C']"
|
||||
@click="
|
||||
() => {
|
||||
copyRequest()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
:icon="IconLink2"
|
||||
:label="`${t('request.view_my_links')}`"
|
||||
to="/profile"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
<HoppSmartItem
|
||||
ref="saveRequestAction"
|
||||
:label="`${t('request.save_as')}`"
|
||||
:icon="IconFolderPlus"
|
||||
:shortcut="['S']"
|
||||
@click="
|
||||
() => {
|
||||
showSaveRequestModal = true
|
||||
@@ -231,51 +225,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconFileCode from "~icons/lucide/file-code"
|
||||
import IconCode2 from "~icons/lucide/code-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||
import IconLink2 from "~icons/lucide/link-2"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import {
|
||||
updateRESTResponse,
|
||||
restEndpoint$,
|
||||
setRESTEndpoint,
|
||||
restMethod$,
|
||||
updateRESTMethod,
|
||||
resetRESTRequest,
|
||||
useRESTRequestName,
|
||||
getRESTSaveContext,
|
||||
getRESTRequest,
|
||||
restRequest$,
|
||||
setRESTSaveContext,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||
import {
|
||||
useStream,
|
||||
useStreamSubscriber,
|
||||
useReadonlyStream,
|
||||
} from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import { useStreamSubscriber } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import {
|
||||
cancelRunningExtensionRequest,
|
||||
hasExtensionInstalled,
|
||||
} from "~/helpers/strategies/ExtensionStrategy"
|
||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||
import IconCode2 from "~icons/lucide/code-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconFileCode from "~icons/lucide/file-code"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconLink2 from "~icons/lucide/link-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -296,9 +279,19 @@ const toast = useToast()
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
|
||||
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
const newEndpoint = computed(() => {
|
||||
return tab.value.document.request.endpoint
|
||||
})
|
||||
const newMethod = computed(() => {
|
||||
return tab.value.document.request.method
|
||||
})
|
||||
|
||||
const curlText = ref("")
|
||||
const newMethod = useStream(restMethod$, "", updateRESTMethod)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -327,6 +320,30 @@ watch(loading, () => {
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: make this oAuthURL() work
|
||||
|
||||
// function oAuthURL() {
|
||||
// const auth = useReadonlyStream(props.request.auth$, {
|
||||
// authType: "none",
|
||||
// authActive: true,
|
||||
// })
|
||||
|
||||
// const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
|
||||
// onBeforeMount(async () => {
|
||||
// try {
|
||||
// const tokenInfo = await oauthRedirect()
|
||||
// if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||
// if (typeof tokenInfo === "object") {
|
||||
// oauth2Token.value = tokenInfo.access_token
|
||||
// }
|
||||
// }
|
||||
|
||||
// // eslint-disable-next-line no-empty
|
||||
// } catch (_) {}
|
||||
// })
|
||||
// }
|
||||
|
||||
const newSendRequest = async () => {
|
||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||
toast.error(`${t("empty.endpoint")}`)
|
||||
@@ -335,10 +352,14 @@ const newSendRequest = async () => {
|
||||
|
||||
ensureMethodInEndpoint()
|
||||
|
||||
console.log("Sending request", newEndpoint.value)
|
||||
|
||||
loading.value = true
|
||||
|
||||
// Double calling is because the function returns a TaskEither than should be executed
|
||||
const streamResult = await runRESTRequest$()()
|
||||
const streamResult = await runRESTRequest$(tab)()
|
||||
|
||||
console.log("Stream result", streamResult)
|
||||
|
||||
if (isRight(streamResult)) {
|
||||
subscribeToStream(
|
||||
@@ -380,9 +401,11 @@ const ensureMethodInEndpoint = () => {
|
||||
) {
|
||||
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||
setRESTEndpoint("http://" + newEndpoint.value)
|
||||
tab.value.document.request.endpoint =
|
||||
"http://" + tab.value.document.request.endpoint
|
||||
} else {
|
||||
setRESTEndpoint("https://" + newEndpoint.value)
|
||||
tab.value.document.request.endpoint =
|
||||
"https://" + tab.value.document.request.endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,7 +418,7 @@ const onPasteUrl = (e: { pastedValue: string; prevValue: string }) => {
|
||||
if (isCURL(pastedData)) {
|
||||
showCurlImportModal.value = true
|
||||
curlText.value = pastedData
|
||||
newEndpoint.value = e.prevValue
|
||||
tab.value.document.request.endpoint = e.prevValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,19 +428,28 @@ function isCURL(curl: string) {
|
||||
|
||||
const cancelRequest = () => {
|
||||
loading.value = false
|
||||
if (hasExtensionInstalled()) {
|
||||
cancelRunningExtensionRequest()
|
||||
}
|
||||
updateRESTResponse(null)
|
||||
}
|
||||
|
||||
const updateMethod = (method: string) => {
|
||||
updateRESTMethod(method)
|
||||
tab.value.document.request.method = method
|
||||
}
|
||||
|
||||
const onSelectMethod = (method: string) => {
|
||||
updateMethod(method)
|
||||
const onSelectMethod = (e: Event | any) => {
|
||||
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
|
||||
updateMethod(e.value)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
resetRESTRequest()
|
||||
tab.value.document.request = getDefaultRESTRequest()
|
||||
}
|
||||
|
||||
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||
tab.value.response = response
|
||||
console.log("Updating response", response)
|
||||
}
|
||||
|
||||
const copyLinkIcon = refAutoReset<
|
||||
@@ -437,20 +469,13 @@ const shareButtonText = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const request = useReadonlyStream(restRequest$, getRESTRequest())
|
||||
|
||||
watch(request, () => {
|
||||
shareLink.value = null
|
||||
})
|
||||
|
||||
const copyRequest = async () => {
|
||||
if (shareLink.value) {
|
||||
copyShareLink(shareLink.value)
|
||||
} else {
|
||||
shareLink.value = ""
|
||||
fetchingShareLink.value = true
|
||||
const request = getRESTRequest()
|
||||
const shortcodeResult = await createShortcode(request)()
|
||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||
if (E.isLeft(shortcodeResult)) {
|
||||
toast.error(`${shortcodeResult.left.error}`)
|
||||
shareLink.value = `${t("error.something_went_wrong")}`
|
||||
@@ -508,33 +533,26 @@ const cycleDownMethod = () => {
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
const saveCtx = getRESTSaveContext()
|
||||
const saveCtx = tab.value.document.saveContext
|
||||
|
||||
if (!saveCtx) {
|
||||
showSaveRequestModal.value = true
|
||||
return
|
||||
}
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
const req = getRESTRequest()
|
||||
const req = tab.value.document.request
|
||||
|
||||
try {
|
||||
editRESTRequest(
|
||||
saveCtx.folderPath,
|
||||
saveCtx.requestIndex,
|
||||
getRESTRequest()
|
||||
)
|
||||
setRESTSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: saveCtx.folderPath,
|
||||
requestIndex: saveCtx.requestIndex,
|
||||
req: cloneDeep(req),
|
||||
})
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||
|
||||
tab.value.document.isDirty = false
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
setRESTSaveContext(null)
|
||||
tab.value.document.saveContext = undefined
|
||||
saveRequest()
|
||||
}
|
||||
} else if (saveCtx.originLocation === "team-collection") {
|
||||
const req = getRESTRequest()
|
||||
const req = tab.value.document.request
|
||||
|
||||
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
||||
try {
|
||||
@@ -548,11 +566,8 @@ const saveRequest = () => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
} else {
|
||||
setRESTSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: saveCtx.requestID,
|
||||
req: cloneDeep(req),
|
||||
})
|
||||
tab.value.document.isDirty = false
|
||||
|
||||
toast.success(`${t("request.saved")}`)
|
||||
}
|
||||
})
|
||||
@@ -584,10 +599,11 @@ defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
|
||||
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
|
||||
|
||||
const isCustomMethod = computed(() => {
|
||||
return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
|
||||
return (
|
||||
tab.value.document.request.method === "CUSTOM" ||
|
||||
!methods.includes(newMethod.value)
|
||||
)
|
||||
})
|
||||
|
||||
const requestName = useRESTRequestName()
|
||||
|
||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||
</script>
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
<template>
|
||||
<SmartTabs
|
||||
<HoppSmartTabs
|
||||
v-model="selectedRealtimeTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
<HoppSmartTab
|
||||
:id="'params'"
|
||||
:label="`${t('tab.parameters')}`"
|
||||
:info="`${newActiveParamsCount$}`"
|
||||
>
|
||||
<HttpParameters />
|
||||
</SmartTab>
|
||||
<SmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
||||
<HttpBody @change-tab="changeTab" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
<HttpParameters v-model="request.params" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
||||
<HttpBody
|
||||
v-model:headers="request.headers"
|
||||
v-model:body="request.body"
|
||||
@change-tab="changeTab"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="`${newActiveHeadersCount$}`"
|
||||
>
|
||||
<HttpHeaders @change-tab="changeTab" />
|
||||
</SmartTab>
|
||||
<SmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<HttpAuthorization />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
<HttpHeaders v-model="request" @change-tab="changeTab" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<HttpAuthorization v-model="request.auth" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'preRequestScript'"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
preRequestScript && preRequestScript.length > 0 ? true : false
|
||||
request.preRequestScript && request.preRequestScript.length > 0
|
||||
? true
|
||||
: false
|
||||
"
|
||||
>
|
||||
<HttpPreRequestScript />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'tests'"
|
||||
:label="`${t('tab.tests')}`"
|
||||
:indicator="testScript && testScript.length > 0 ? true : false"
|
||||
:indicator="
|
||||
request.testScript && request.testScript.length > 0 ? true : false
|
||||
"
|
||||
>
|
||||
<HttpTests />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
<HttpTests v-model="request.testScript" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { map } from "rxjs/operators"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import {
|
||||
restActiveHeadersCount$,
|
||||
restActiveParamsCount$,
|
||||
usePreRequestScript,
|
||||
useTestScript,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, ref, watch } from "vue"
|
||||
|
||||
export type RequestOptionTabs =
|
||||
| "params"
|
||||
@@ -63,33 +64,43 @@ export type RequestOptionTabs =
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = defineProps<{ modelValue: HoppRESTRequest }>()
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => request.value,
|
||||
(newVal) => {
|
||||
emit("update:modelValue", newVal)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
const changeTab = (e: RequestOptionTabs) => {
|
||||
selectedRealtimeTab.value = e
|
||||
}
|
||||
|
||||
const newActiveParamsCount$ = useReadonlyStream(
|
||||
restActiveParamsCount$.pipe(
|
||||
map((e) => {
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
),
|
||||
null
|
||||
)
|
||||
const newActiveParamsCount$ = computed(() => {
|
||||
const e = request.value.params.filter(
|
||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||
).length
|
||||
|
||||
const newActiveHeadersCount$ = useReadonlyStream(
|
||||
restActiveHeadersCount$.pipe(
|
||||
map((e) => {
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
),
|
||||
null
|
||||
)
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
|
||||
const preRequestScript = usePreRequestScript()
|
||||
const newActiveHeadersCount$ = computed(() => {
|
||||
const e = request.value.headers.filter(
|
||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||
).length
|
||||
|
||||
const testScript = useTestScript()
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="rest-primary">
|
||||
<template #primary>
|
||||
<HttpRequest v-model="tab" />
|
||||
<HttpRequestOptions v-model="tab.document.request" />
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpResponse v-model:tab="tab" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
|
||||
|
||||
// TODO: Move Response and Request execution code to over here
|
||||
|
||||
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppRESTTab): 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) => {
|
||||
if (
|
||||
!tab.value.document.isDirty &&
|
||||
!isEqualHoppRESTRequest(oldRequest, updatedValue)
|
||||
) {
|
||||
tab.value.document.isDirty = true
|
||||
}
|
||||
|
||||
oldRequest = cloneDeep(updatedValue)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,42 +1,42 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<HttpResponseMeta :response="response" />
|
||||
<HttpResponseMeta :response="tab.response" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
:response="response"
|
||||
v-model:selected-tab-preference="selectedTabPreference"
|
||||
v-model:tab="tab"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch } from "vue"
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { restResponse$ } from "~/newstore/RESTSession"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const response = useReadonlyStream(restResponse$, null)
|
||||
const props = defineProps<{
|
||||
tab: HoppRESTTab
|
||||
}>()
|
||||
|
||||
const hasResponse = computed(
|
||||
() =>
|
||||
response.value?.type === "success" || response.value?.type === "fail"
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRESTTab): void
|
||||
}>()
|
||||
|
||||
const loading = computed(
|
||||
() => response.value === null || response.value.type === "loading"
|
||||
)
|
||||
const tab = useVModel(props, "tab", emit)
|
||||
|
||||
watch(response, () => {
|
||||
if (response.value?.type === "loading") startPageProgress()
|
||||
else completePageProgress()
|
||||
})
|
||||
const selectedTabPreference = ref<string | null>(null)
|
||||
|
||||
return {
|
||||
hasResponse,
|
||||
response,
|
||||
loading,
|
||||
}
|
||||
},
|
||||
const hasResponse = computed(
|
||||
() =>
|
||||
tab.value.response?.type === "success" ||
|
||||
tab.value.response?.type === "fail"
|
||||
)
|
||||
|
||||
const loading = computed(() => tab.value.response?.type === "loading")
|
||||
|
||||
watch(loading, (isLoading) => {
|
||||
if (isLoading) startPageProgress()
|
||||
else completePageProgress()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-if="response.type === 'loading'"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -107,7 +107,7 @@ const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
response: HoppRESTResponse | null | undefined
|
||||
}>()
|
||||
|
||||
/**
|
||||
@@ -118,6 +118,8 @@ const props = defineProps<{
|
||||
*/
|
||||
const readableResponseSize = computed(() => {
|
||||
if (
|
||||
props.response === null ||
|
||||
props.response === undefined ||
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail" ||
|
||||
props.response.type === "script_fail" ||
|
||||
@@ -135,6 +137,8 @@ const readableResponseSize = computed(() => {
|
||||
|
||||
const statusCategory = computed(() => {
|
||||
if (
|
||||
props.response === null ||
|
||||
props.response === undefined ||
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail" ||
|
||||
props.response.type === "script_fail" ||
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<template>
|
||||
<SmartTabs
|
||||
<HoppSmartTabs
|
||||
v-model="selectedNavigationTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary z-10 top-0"
|
||||
vertical
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab :id="'history'" :icon="IconClock" :label="`${t('tab.history')}`">
|
||||
<History :page="'rest'" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
<HoppSmartTab
|
||||
:id="'collections'"
|
||||
:icon="IconFolder"
|
||||
:label="`${t('tab.collections')}`"
|
||||
>
|
||||
<Collections />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'env'"
|
||||
:icon="IconLayers"
|
||||
:label="`${t('environment.title')}`"
|
||||
:label="`${t('tab.environments')}`"
|
||||
>
|
||||
<Environments />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'history'"
|
||||
:icon="IconClock"
|
||||
:label="`${t('tab.history')}`"
|
||||
>
|
||||
<History :page="'rest'" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -36,5 +40,5 @@ const t = useI18n()
|
||||
|
||||
type RequestOptionTabs = "history" | "collections" | "env"
|
||||
|
||||
const selectedNavigationTab = ref<RequestOptionTabs>("history")
|
||||
const selectedNavigationTab = ref<RequestOptionTabs>("collections")
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("test.report") }}
|
||||
</label>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@@ -48,13 +48,13 @@
|
||||
{{ t("environment.no_environment_description") }}
|
||||
</p>
|
||||
<p class="flex mt-3 space-x-2">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('environment.add_to_global')"
|
||||
class="text-tiny !bg-primary"
|
||||
filled
|
||||
@click="addEnvToGlobal()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
:label="t('environment.create_new')"
|
||||
class="text-tiny !bg-primary"
|
||||
filled
|
||||
@@ -186,7 +186,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("helpers.tests") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="`${t('action.learn_more')}`"
|
||||
to="https://docs.hoppscotch.io/features/tests"
|
||||
@@ -216,7 +216,6 @@ import {
|
||||
setGlobalEnvVariables,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { restTestResults$, setRESTTestResults } from "~/newstore/RESTSession"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
@@ -226,6 +225,17 @@ import IconCheck from "~icons/lucide/check"
|
||||
import IconClose from "~icons/lucide/x"
|
||||
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTestResult | null | undefined): void
|
||||
}>()
|
||||
|
||||
const testResults = useVModel(props, "modelValue", emit)
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -236,11 +246,6 @@ const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
showModalDetails.value = shouldDisplay
|
||||
}
|
||||
|
||||
const testResults = useReadonlyStream(
|
||||
restTestResults$,
|
||||
null
|
||||
) as Ref<HoppTestResult | null>
|
||||
|
||||
/**
|
||||
* Get the "addition" environment variables
|
||||
* @returns Array of objects with key-value pairs of arguments
|
||||
@@ -250,7 +255,9 @@ const getAdditionVars = () =>
|
||||
? testResults.value.envDiff.selected.additions
|
||||
: []
|
||||
|
||||
const clearContent = () => setRESTTestResults(null)
|
||||
const clearContent = () => {
|
||||
testResults.value = null
|
||||
}
|
||||
|
||||
const haveEnvVariables = computed(() => {
|
||||
if (!testResults.value) return false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2">
|
||||
<SmartProgressRing
|
||||
<HoppSmartProgressRing
|
||||
class="mr-2 text-red-500"
|
||||
:radius="8"
|
||||
:stroke="1.5"
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("test.javascript_code") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/tests"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.post_request_tests") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
<HoppSmartAnchor
|
||||
:label="`${t('test.learn')}`"
|
||||
to="https://docs.hoppscotch.io/features/tests"
|
||||
blank
|
||||
@@ -66,17 +66,20 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { reactive, ref } from "vue"
|
||||
import { useTestScript } from "~/newstore/RESTSession"
|
||||
import testSnippets from "~/helpers/testSnippets"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import linter from "~/helpers/editor/linting/testScript"
|
||||
import completer from "~/helpers/editor/completion/testScript"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const testScript = useTestScript()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const testScript = useVModel(props, "modelValue", emit)
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
{{ t("request.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
@@ -28,14 +28,14 @@
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@@ -61,7 +61,7 @@
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
@@ -104,7 +104,7 @@
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
param.hasOwnProperty('active')
|
||||
@@ -132,7 +132,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
@@ -156,7 +156,7 @@
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.body") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
@@ -181,6 +181,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { isEqual, cloneDeep } from "lodash-es"
|
||||
import {
|
||||
HoppRESTReqBody,
|
||||
parseRawKeyValueEntries,
|
||||
parseRawKeyValueEntriesE,
|
||||
rawKeyValueEntriesToString,
|
||||
@@ -191,16 +192,30 @@ import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import * as E from "fp-ts/Either"
|
||||
import draggable from "vuedraggable"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
type Body = HoppRESTReqBody & {
|
||||
contentType: "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Body
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: Body): void
|
||||
}>()
|
||||
|
||||
const body = useVModel(props, "modelValue", emit)
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -231,7 +246,7 @@ useCodemirror(
|
||||
)
|
||||
|
||||
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
|
||||
const urlEncodedParamsRaw = pluckRef(useRESTRequestBody(), "body")
|
||||
const urlEncodedParamsRaw = pluckRef(body, "body")
|
||||
|
||||
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
||||
get() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user