Compare commits

..

45 Commits

Author SHA1 Message Date
Akash K
eeee8af806 chore: changes to support id based syncing for collections (#2977) 2023-04-11 14:30:01 +05:30
Andrew Bastin
134441a6e7 chore: update CODEOWNERS 2023-04-09 21:54:51 +05:30
Andrew Bastin
80a5d21576 chore: set web and common versions to 2023.4.0 and remove workspace package version specifiers 2023-04-09 21:42:25 +05:30
Akash K
f1a812dae2 refactor: add optional ids to environments (#2974) 2023-04-09 14:30:42 +05:30
Anwarul Islam
65a194a6d2 fix: shortcode data do not fetch or render (#2972) 2023-04-07 03:11:07 +05:30
Anwarul Islam
55e3dd3c18 fix: reset save context issue on delete collection or folder of team (#2971) 2023-04-07 03:06:30 +05:30
Nivedin
b88f496f4e fix: team environment bug when logout (#2970) 2023-04-07 03:00:30 +05:30
Anwarul Islam
8caf9f110b feat: added scrollbar for tabs (#2969) 2023-04-06 15:20:16 +05:30
Akash K
1370b53726 chore: enable/disable features in platforms (#2968) 2023-04-06 15:06:33 +05:30
Nivedin
8590a9a110 fix: reordering last request bug and its UX (#2967)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-05 21:35:14 +05:30
Anwarul Islam
62058d5dfe fix: tabhead and scrolling issue (#2966)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-05 21:26:42 +05:30
Nivedin
1d397af674 fix: move collection and request to bottom of list (#2964) 2023-04-05 15:38:57 +05:30
Nivedin
141a468808 chore: update header UI (#2965) 2023-04-05 15:30:47 +05:30
Akash K
a24d724e2b chore: load terms of service & privacy policy links from env variables (#2963) 2023-04-04 20:59:31 +05:30
Anwarul Islam
dd72eacd21 fix: no page rendering issue after navigating from rest page (#2962) 2023-04-04 20:48:32 +05:30
Akash K
c3c3fc6720 chore: use IDs instead of Strings in graphql queries (#2961) 2023-04-04 04:10:32 +05:30
Akash K
37a3b72025 chore: move analytics to platform (#2960) 2023-04-04 02:15:20 +05:30
Anwarul Islam
defece95fc feat: rest revamp (#2918)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-31 00:45:42 +05:30
Akash K
dbb45e7253 chore: add removeDuplicateEntry dispatcher to history store (#2955) 2023-03-30 15:13:25 +05:30
Akash K
cc802b1e9f chore: move history firebase things to hoppscotch-web (#2954) 2023-03-30 00:09:08 +05:30
Akash K
a66a2f5645 chore: move settings firebase things to platform (#2953) 2023-03-29 23:59:28 +05:30
Akash K
39afeab5f8 refactor: make editFolder, editCollection take Partial collection as parameter (#2952) 2023-03-29 12:00:20 +05:30
Andrew Bastin
1583c86c78 chore: add dotenv as dev dependency to fix staging issues 2023-03-14 22:32:28 +05:30
Akash K
1372681b87 refactor: store changes for collections (#2947) 2023-03-14 14:16:45 +05:30
Nivedin
2179ce6fff fix: reordering bugs and UX fixes (#2948) 2023-03-14 14:01:47 +05:30
Akash K
7e1b26c6a9 chore: move collections sync system to platform (#2940) 2023-03-13 17:08:05 +05:30
Akash K
ae9b7183b5 refactor: optional variables to createEnvironment and fixing the order of initializing GqlClient (#2944) 2023-03-07 16:12:11 +05:30
Akash K
3fa4052538 chore: move environments firebase things to hoppscotch-web (#2939) 2023-03-02 18:50:13 +05:30
Liyas Thomas
f2de0dc673 chore: minor ui improvements 2023-02-28 16:25:46 +05:30
Nivedin
7e686a8882 feat: global workspace selector (#2922)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-02-24 23:20:02 +05:30
Nivedin
4ca6e9ec3a feat: added reordering and moving for collection (#2916) 2023-02-24 19:09:07 +05:30
Andrew Bastin
dcd441f15e fix: link not rendering and UI storybook build issues 2023-02-24 15:51:10 +05:30
Andrew Bastin
90c8fbeee4 fix: issues with ui histoire building and modal not having close button 2023-02-24 14:35:42 +05:30
Andrew Bastin
cae1840506 refactor: update hopp-ui to be independent (#2927)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-02-24 13:20:12 +05:30
Jesvin Jose
82c6f6f6bc fix: response time for requests via extension has incorrect value (#2921)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-02-17 19:14:50 +05:30
Andrew Bastin
2545262fc2 chore: improve DispatchingStore typings for dispatch streams 2023-02-17 16:04:29 +05:30
Andrew Bastin
b27fe871c4 refactor: improve type checking for DispatchingStore dispatch payloads 2023-02-14 14:17:00 +05:30
Andrew Bastin
cb5fff0310 fix: graphql collections not syncing on login 2023-02-14 10:29:43 +05:30
Liyas Thomas
d15caba4a6 chore: improve ui responsiveness 2023-02-08 18:57:52 +05:30
Liyas Thomas
536c8128dd docs: update package description [skip ci] 2023-02-08 18:50:32 +05:30
Andrew Bastin
99918ee0c0 chore: prettier version bump and related fixes 2023-02-08 18:35:27 +05:30
Liyas Thomas
864d40d934 chore: improved theme colors 2023-02-08 15:13:24 +05:30
Jesvin Jose
a227af05d9 feat: active tab no longer resets after request (#2917)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Closes https://github.com/hoppscotch/hoppscotch/issues/2080
2023-02-08 10:29:18 +05:30
Andrew Bastin
ce0898956d chore: reintroduce updated auth mechanism 2023-02-07 19:21:06 +05:30
Jesvin Jose
cd72851289 refactor: cli updates (#2907)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-02-07 17:47:54 +05:30
278 changed files with 11666 additions and 8578 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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");
});

View File

@@ -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();
});
});

View File

@@ -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",

View File

@@ -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();
});
});

View File

@@ -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
}
};

View File

@@ -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);
}
};
};

View File

@@ -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",
})
}
}

View File

@@ -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 }
}

View File

@@ -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";

View File

@@ -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

View File

@@ -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);

View File

@@ -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>[]
};

View File

@@ -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

View File

@@ -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 {

View File

@@ -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";
}

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -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 />

View File

@@ -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']
}
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -32,7 +32,7 @@
</div>
</div>
</div>
<ButtonSecondary
<HoppButtonSecondary
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/features/response"
:icon="IconExternalLink"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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">

View File

@@ -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">

View File

@@ -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
}
}
)

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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,
},
},
})),

View File

@@ -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>

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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")}`)
}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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"))
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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()
}
}
)

View File

@@ -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")}`)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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">

View File

@@ -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(

View File

@@ -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

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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"

View File

@@ -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(),
})

View File

@@ -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(),
})

View File

@@ -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'

View File

@@ -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">

View File

@@ -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"

View File

@@ -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 = (

View File

@@ -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 }"

View File

@@ -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>

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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)
}
}
)

View File

@@ -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,

View File

@@ -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")}`)

View File

@@ -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>

View File

@@ -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 }>>([

View File

@@ -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)

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" ||

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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