Compare commits

...

108 Commits

Author SHA1 Message Date
Liyas Thomas
a795fc4310 release: v1.12.0 2021-05-27 15:11:01 +00:00
Liyas Thomas
790b743e42 fix: copy .env to root - fixed #1671 2021-05-27 14:08:21 +00:00
Andrew Bastin
21aeded2ea Update DispatchingStore to provide better typing for dispatch function parameters 2021-05-25 18:27:50 -04:00
liyasthomas
baf6d6bd29 refactor: lint options 2021-05-25 21:43:13 +05:30
Andrew Bastin
2e213a8692 Remove History test spec 2021-05-24 23:37:48 -04:00
Andrew Bastin
455cccebb5 Fix issue with clear history confirmation persisting after clearing 2021-05-24 23:37:48 -04:00
Andrew Bastin
9e602394cf Remove dependency on history component to add history entry 2021-05-24 23:37:48 -04:00
Andrew Bastin
46ebd49936 Refactor history entry limits 2021-05-24 23:37:48 -04:00
Andrew Bastin
fcac750ad7 Migrate code to new history store 2021-05-24 23:37:48 -04:00
Andrew Bastin
8cd3acd205 Introduce History store 2021-05-24 23:37:48 -04:00
liyasthomas
bc95a028ce chore(deps): bump 2021-05-23 18:48:48 +05:30
Andrew Bastin
5fb457d331 Fix Settings Sync issues 2021-05-22 21:48:15 -04:00
liyasthomas
454c11a12c chore: lint 2021-05-22 20:50:23 +05:30
Andrew Bastin
3ac9a418e6 Defer ChooseType teams list query until component visible 2021-05-20 22:21:12 -04:00
liyasthomas
c98de5988e fix: clean input value in modals - fixed #1662 2021-05-20 14:24:14 +05:30
Andrew Bastin
ca5df588b7 Fix UrlField tests 2021-05-19 22:41:16 -04:00
Andrew Bastin
08a41691af Revamp URLField 2021-05-19 22:25:00 -04:00
Andrew Bastin
665f272a0e Stylelint updates 2021-05-19 22:24:31 -04:00
Andrew Bastin
2591e3ab67 Add node-interval-tree as dep 2021-05-19 22:23:47 -04:00
liyasthomas
44df9b3be8 refactor: lint 2021-05-19 10:24:57 +05:30
liyasthomas
40ddfa8def refactor: lint 2021-05-18 21:39:55 +05:30
liyasthomas
cc27c552af refactor: lint 2021-05-18 14:57:29 +05:30
liyasthomas
7f248da0b3 refactor: lint 2021-05-18 11:56:59 +05:30
Andrew Bastin
79a0002594 Load through server side pagination for team members list 2021-05-17 12:27:36 -04:00
Andrew Bastin
a6c015bcc5 Make ESLint happy with fb.js 2021-05-17 12:26:57 -04:00
Andrew Bastin
692d98cb55 Make ESLint a bit more forgiving 2021-05-17 12:26:25 -04:00
liyasthomas
d9ddc184cb refactor: lint 2021-05-17 19:17:57 +05:30
liyasthomas
e424d06026 refactor: lint 2021-05-17 17:11:58 +05:30
liyasthomas
f9821e5f80 feat: eslint for typescript 2021-05-17 15:17:23 +05:30
Liyas Thomas
5bd53dc093 feat: eslint + stylelint 2021-05-17 05:55:50 +00:00
Liyas Thomas
204834872e refector: remove unused components 2021-05-17 03:19:53 +00:00
liyasthomas
a24049aa17 fix: dotenv 2021-05-16 14:10:39 +05:30
liyasthomas
9cb9e9e7b6 fix: dotenv 2021-05-16 12:53:40 +05:30
liyasthomas
3e7a766d12 Merge branch 'main' of https://github.com/hoppscotch/hoppscotch 2021-05-16 12:23:45 +05:30
liyasthomas
02debdc28b fix: use .env values - fixed #1639 2021-05-16 12:23:29 +05:30
liyasthomas
e104fe3021 chore(deps): bump 2021-05-16 12:21:28 +05:30
Andrew Bastin
dda40537cc Don't update BackendUserInfo if not authenticated 2021-05-15 21:52:31 -04:00
liyasthomas
217269467c docs: updated sponsors list 2021-05-16 04:08:56 +05:30
Liyas Thomas
5f193680c9 fix: minor UI stylings 2021-05-15 21:19:23 +00:00
Liyas Thomas
c0ef713c0b i18n: add Hindi and Norwegian (Bokmål) language (#1642)
Co-authored-by: Sindre van der Linden <sindre@allvis.io>
Co-authored-by: Deven Rathod <devenrathod2001@gmail.com>
2021-05-15 06:23:12 -07:00
Liyas Thomas
3533aa391a chore: lint 2021-05-15 12:43:31 +00:00
Liyas Thomas
ec90365427 Merge pull request #1641 from hoppscotch/teams 2021-05-15 14:08:03 +05:30
Liyas Thomas
78f7a9ba06 chore(deps): lock file 2021-05-15 08:26:22 +00:00
Liyas Thomas
7793dd8b3e Merge branch 'main' into teams 2021-05-15 08:11:03 +00:00
Liyas Thomas
76866f78f0 cleanup: remove logs 2021-05-15 06:47:13 +00:00
Liyas Thomas
8e3ecb4c25 fix: save location picker indicator 2021-05-15 05:45:05 +00:00
Andrew Bastin
1b9b2ac4c9 Fix SaveRequest modal selection 2021-05-15 00:02:55 -04:00
Andrew Bastin
489f14b88f Fixed issue in TeamCollectionAdapter with request updates 2021-05-14 01:59:08 -04:00
Liyas Thomas
8f09c82763 fixes and refactor 2021-05-14 05:50:17 +00:00
Andrew Bastin
2b8cda40a2 Introduce TeamMemberAdapter 2021-05-14 00:08:52 -04:00
Liyas Thomas
4656d67fcf fix: teams edit modal + race conditions on currentUser 2021-05-14 03:40:53 +00:00
Andrew Bastin
e7dd67deaa Update race condition with Apollo getting ID Token and BackendUserInfo 2021-05-13 22:42:04 -04:00
Andrew Bastin
b9c3219094 Hide "Team Collections" tab if user has no access 2021-05-13 20:16:41 -04:00
Andrew Bastin
f55a995c0a Introducing BackendUserInfo 2021-05-13 20:16:29 -04:00
Andrew Bastin
ee378a128c Introduce observable function for team member list 2021-05-13 02:28:35 -04:00
Liyas Thomas
c1f083d19f fix: indicate selected location on save request 2021-05-13 04:26:33 +00:00
Liyas Thomas
2ff0f97295 refactor: request actions + fix: timeout bug 2021-05-12 05:33:15 +00:00
Andrew Bastin
dd3b51d0b7 Use Intersection component to fetch team list on visibility 2021-05-11 23:39:23 -04:00
Andrew Bastin
853acfda2c Introduce Intersection component 2021-05-11 23:38:47 -04:00
Liyas Thomas
c756be54a1 refactor: separate request components for team & my collections 2021-05-11 13:10:36 +00:00
Liyas Thomas
3bbf334f99 refactor: separate folders for team and personal collections 2021-05-11 05:49:36 +00:00
Liyas Thomas
c6c8b7da6a chore(deps): bump 2021-05-09 14:59:24 +00:00
Liyas Thomas
8a30d3ed42 refactor: separated my collections and tam collections 2021-05-09 07:48:52 +00:00
Andrew Bastin
a5a6b864a5 Fix issue with child collection not rerendering 2021-05-09 02:08:48 -04:00
Liyas Thomas
97b7320939 move collections actions to parent 2021-05-09 05:30:31 +00:00
Andrew Bastin
5c11bcb2c6 Update TeamCollectionAdapter with more docs 2021-05-06 21:22:17 -04:00
Andrew Bastin
3888584b58 Initial refactor of the collections list and friends 2021-05-06 20:46:29 -04:00
Andrew Bastin
531a9c0bc8 Update team_utils imports 2021-05-06 12:21:53 -04:00
Andrew Bastin
84f8048e0a Initial introduction of TeamCollectionAdapter based collection render 2021-05-06 12:06:19 -04:00
Andrew Bastin
fdf96b0b63 Fixed build time issue with TeamCollectionAdapter 2021-05-06 12:05:45 -04:00
Andrew Bastin
86ddfe2c9f Introduce TeamCollectionAdapter 2021-05-05 10:44:17 -04:00
Andrew Bastin
115ae93073 Merge pull request #1625 from iamtabrezkhan/issue-1507
FEAT: add syntax highlighting in Code Generator Modal
2021-05-04 22:57:45 -04:00
Andrew Bastin
aa65cada6f Merge branch 'main' into issue-1507 2021-05-04 22:44:11 -04:00
Andrew Bastin
ad9cdf3fa8 Update team/utils code style 2021-05-04 22:18:46 -04:00
Liyas Thomas
5c0035f4c3 chore(deps): bump 2021-05-04 19:36:50 +00:00
Liyas Thomas
37b4c67ead docs: updated sponsors list 2021-05-04 19:36:23 +00:00
Liyas Thomas
63998f4a23 minor UI styling 2021-05-04 14:02:19 +05:30
Liyas Thomas
efeec13794 set default export language as cURL 2021-05-03 19:14:26 -07:00
Liyas Thomas
6c49e5c86a fix: race condition where no language is selected 2021-05-03 19:12:13 -07:00
Andrew Bastin
ec57392424 Fixed subscriptions authorization 2021-05-03 09:57:58 -04:00
Andrew Bastin
8713aa7fd3 Remove @nuxtjs/apollo and added vue-apollo as dep 2021-05-03 09:22:39 -04:00
Andrew Bastin
627811f28d Refactor Apollo Client handling 2021-05-03 09:18:24 -04:00
Tabrez Khan
3f743b4f61 FEAT: add syntax highlighting in Code Generator Modal
USE: SmartAceEditor componenent
2021-05-03 14:46:41 +05:30
Liyas Thomas
d676b5a68d chore(deps): bump rxjs 2021-05-03 05:08:39 +00:00
Liyas Thomas
a6dfab5fbf chore(deps): bump 2021-05-02 15:21:27 +00:00
Liyas Thomas
b8bc110d33 check existance of teamCollectionAdded.parent
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-05-02 03:49:00 +00:00
Andrew Bastin
597fd2c6d1 Fix duplication on teams folder creation 2021-04-29 23:50:50 -04:00
Liyas Thomas
ab0bc3e927 fix: permissions for collections and folders 2021-04-30 01:04:41 +00:00
liyasthomas
e28373dae1 fixes
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-04-29 14:03:39 +05:30
Liyas Thomas
1bc57f159c merge feat/teams-new-ui
Co-authored-by: Isha Gupta <40794215+IshaGupta18@users.noreply.github.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Osheen Sachdev <45964755+oshhh@users.noreply.github.com>
Co-authored-by: Rohan Rajpal <rohan46000@gmail.com>
Co-authored-by: Raghav Gupta <raghav.gupta0307@gmail.com>
2021-04-26 09:37:18 +00:00
Liyas Thomas
6a8019c7a0 chore(deps): bump 2021-04-25 16:38:09 +00:00
Liyas Thomas
9b0dc5a849 Merge pull request #1615 from hoppscotch/feat/links 2021-04-23 04:46:54 -07:00
Liyas Thomas
283e1b0790 chore(deps): bump 2021-04-23 11:39:20 +00:00
Liyas Thomas
fc05a4bb78 docs: better copy 2021-04-23 11:38:56 +00:00
Liyas Thomas
88b32e317a docs: update links 2021-04-23 11:38:17 +00:00
Liyas Thomas
6c61a70d58 docs: updated built with stack, ideas for externship 2021-04-22 01:01:39 +00:00
Osheen Sachdev
5df3ebae22 Documentation generation (#1610)
Co-authored-by: Isha Gupta <40794215+IshaGupta18@users.noreply.github.com>
Co-authored-by: IshaGupta18 <ishagupta1828@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-04-21 10:17:31 -07:00
Liyas Thomas
ad252476ce docs: update GitHub Externship guidelines 2021-04-20 11:20:01 +00:00
Liyas Thomas
8e5f6a3a96 GitHub Externship @ Hoppscotch 2021-04-20 10:37:46 +00:00
Liyas Thomas
9ff209bd32 Merge pull request #1608 from hoppscotch/dependabot/npm_and_yarn/ssri-6.0.2
chore(deps): bump ssri from 6.0.1 to 6.0.2
2021-04-19 09:27:15 -07:00
dependabot[bot]
505e61475d chore(deps): bump ssri from 6.0.1 to 6.0.2
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-19 16:22:02 +00:00
Liyas Thomas
2c137a0eec chore(deps): bump 2021-04-18 15:09:32 +00:00
Liyas Thomas
b65ae89be6 fix: respect environment variable in inline query parameters - fixed #1603 2021-04-15 10:04:23 +00:00
Liyas Thomas
432884a2af Select multiple collections (#1604) (#1605) 2021-04-15 01:01:15 -07:00
Osheen Sachdev
6878498b2e Fix folder nesting in document generation (#1602) 2021-04-14 17:54:55 -07:00
Jainal Gosaliya
4d5332fef7 fixes empty url bug during importing postman json (#1593)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-04-13 21:44:50 -07:00
Andrew Bastin
50a149b662 Bump graphql-language-service-interface and fix build issues 2021-04-12 11:50:25 -04:00
Liyas Thomas
930838d676 chore(deps): bump 2021-04-12 08:14:19 +00:00
225 changed files with 64330 additions and 9472 deletions

View File

@@ -30,7 +30,7 @@ coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)

View File

@@ -1,4 +1,5 @@
# editorconfig.org
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
@@ -8,6 +9,3 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -1,15 +1,18 @@
# Google Analytics
GA_ID=UA-XXXXXXXX-X
# Google Analytics ID
GA_ID=UA-61422507-4
# Google Tag Manager
GTM_ID=GTM-XXXXXXX
# Google Tag Manager ID
GTM_ID=GTM-NMKVBMV
# Firebase
API_KEY=api-key
AUTH_DOMAIN=project-id.firebaseapp.com
DATABASE_URL=https://project-id.firebaseio.com
PROJECT_ID=project-id
STORAGE_BUCKET=project-id.appspot.com
MESSAGING_SENDER_ID=sender-id
APP_ID=app-id
MEASUREMENT_ID=G-measurement-id
# Firebase config
API_KEY=AIzaSyCMsFreESs58-hRxTtiqQrIcimh4i1wbsM
AUTH_DOMAIN=postwoman-api.firebaseapp.com
DATABASE_URL=https://postwoman-api.firebaseio.com
PROJECT_ID=postwoman-api
STORAGE_BUCKET=postwoman-api.appspot.com
MESSAGING_SENDER_ID=421993993223
APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
MEASUREMENT_ID=G-ERJ6025CEB
# Base URL
BASE_URL=https://hoppscotch.io

49
.eslintrc.js Normal file
View File

@@ -0,0 +1,49 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
},
extends: [
"@nuxtjs",
"@nuxtjs/eslint-config-typescript",
"prettier/prettier",
"eslint:recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
"plugin:nuxt/recommended",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"vue/max-attributes-per-line": "off",
"vue/component-name-in-template-casing": ["error", "PascalCase"],
"vue/html-self-closing": [
"error",
{
html: {
normal: "never",
void: "always",
},
},
],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/require-default-prop": "warn",
"vue/require-prop-types": "warn",
"prettier/prettier": ["warn", { semi: false }],
"import/no-named-as-default": "off",
"no-undef": "off",
},
globals: {
$nuxt: true,
},
}

2
.gitignore vendored
View File

@@ -26,7 +26,7 @@ coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
. "$(dirname "$0")/_/husky.sh"
npm run pretty-quick
npm run pre-commit

View File

@@ -4,4 +4,5 @@
.hoppscotch
.vscode
package-lock.json
node_modules
node_modules
dist

3
.prettierrc.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
semi: false
}

24
.stylelintrc.js Normal file
View File

@@ -0,0 +1,24 @@
module.exports = {
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
// add your custom config here
// https://stylelint.io/user-guide/configuration
rules: {
"at-rule-no-unknown": [
true,
{
ignoreAtRules: [
"extends",
"tailwind",
"apply",
"variants",
"responsive",
"screen",
"mixin",
"include",
],
},
],
"declaration-block-trailing-semicolon": null,
"no-descending-specificity": null,
},
}

View File

@@ -1,5 +1,13 @@
# Changelog
## [v1.12.0](https://github.com/hoppscotch/hoppscotch/tree/v1.12.0) (2020-05-27)
[Full Changelog](https://github.com/hoppscotch/hoppscotch/compare/v1.10.0...v1.12.0)
## [v1.10.0](https://github.com/hoppscotch/hoppscotch/tree/v1.10.0) (2020-04-10)
[Full Changelog](https://github.com/hoppscotch/hoppscotch/compare/v1.9.9...v1.10.0)
## [v1.9.9](https://github.com/liyasthomas/postwoman/tree/v1.9.9) (2020-07-30)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.9.7...v1.9.9)

View File

@@ -2,75 +2,131 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
Examples of behavior that contributes to a positive environment for our
community include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
## Enforcement Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior. Maintainers are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned with our Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at liyascthomas@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -12,81 +12,6 @@ Please note we have a code of conduct, please follow it in all your interactions
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer merge it for you.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior. Maintainers are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned with our Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at liyascthomas@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,6 +1,6 @@
FROM node:12.10.0-alpine
LABEL maintainer="Liyas Thomas (liyascthomas@gmail.com)"
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
# Add git as the prebuild target requires it to parse version information
RUN apk add --update --no-cache \
@@ -17,9 +17,9 @@ ADD . /app/
COPY . .
RUN npm run generate
ENV HOST 0.0.0.0
EXPOSE 3000
RUN mv .env.example .env
CMD ["npm", "run", "dev"]

109
GITHUB_EXTERNSHIP.md Normal file
View File

@@ -0,0 +1,109 @@
# **GitHub Externship @ Hoppscotch**
**We ❤️ open source**
We are super excited to be a Partner Organization of GitHub Externship 2021 and look forward to supporting all interested students to participate and contribute to our [open source projects](https://github.com/hoppscotch).
This document covers the process, expectations and guidelines for any interested student so that we can guide, mentor in the best possible manner and most importantly review your community engagement which is a critical factor along with your project idea to be selected from our organization.
**This post will be updated from time to time. So please keep an eye on it.**
Before, any of you jump in and start contributing, here are a few documents from the GitHub Externship Team which we advise every student to read through carefully and thoroughly:
- [GitHub Externship: Website](https://github-externships.github.io/externship/index.html)
- [GitHub Externship: Program Structure](https://github-externships.github.io/externship/structure.html)
Our awesome community members frequently request for features on our [Discord server](https://hoppscotch.io/discord), [Telegram group](https://hoppscotch.io/externship_telegram), [GitHub issues tab](https://github.com/hoppscotch/hoppscotch/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and we have a never ending list of awesome ideas. However, keeping in mind the 3 months time span, we have filtered through our stockpile and come up with the following ideas for [GitHub Externship](https://github-externships.github.io/externship/index.html) program.
## **Ideas**
If youre a student interested in working with Hoppscotch during GitHub Externship, weve assembled for you a list of 20 ideas under the following three categories:
### **1. Implement existing feature requests:**
- [gRPC-Web support](https://github.com/hoppscotch/hoppscotch/issues/402)
Like swagger, let users view message formats from `.proto` / `URL` and test the server methods by sending any messages from the Hoppscotch.
[**`Learn more →`**](https://github.com/hoppscotch/hoppscotch/issues/402)
- [Swagger / OpenAPI support](https://github.com/hoppscotch/hoppscotch/issues/470)
Like [Swagger Inspector](https://inspector.swagger.io/builder) / [Swagger Editor](https://editor.swagger.io). Let users import `swagger.json` and test the server methods by sending any messages from the Hoppscotch.
[**`Learn more →`**](https://github.com/hoppscotch/hoppscotch/issues/470)
- [Mock Server](https://github.com/hoppscotch/hoppscotch/issues/1598)
Mock server allows users to create mock endpoints that accepts request from the system-under-test or from a `json` file.
[**`Learn more →`**](https://github.com/hoppscotch/hoppscotch/issues/1598)
### **2. Implement new features:**
If you'd like to implement a totally new feature to help developers make API testing easy, feel free to come up with your own feature request or a port of feature from other API testing tools.
[**Create new feature request →**](https://github.com/hoppscotch/hoppscotch/issues/new?assignees=&labels=&template=feature_request.md&title=Feature%20[GitHub%20Externship]:)
### **3. Refactor existing features:**
If you'd like to refactor an already implemented feature / workflow - feel free to open a PR to do that. We appreciate using industry best pratices and latest technologies on all of our open source projects.
## **Built with**
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Tailwind CSS](https://tailwindcss.com)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org)
- [Nuxt](https://nuxtjs.org)
## **Contributing**
Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/hoppscotch/hoppscotch/compare).
Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
## **Quick links**
[**`Repository →`**](https://github.com/hoppscotch/hoppscotch)
[**`Web app →`**](https://hoppscotch.io)
[**`Discord server →`**](https://hoppscotch.io/discord)
[**`Telegram group →`**](https://hoppscotch.io/externship_telegram)
## **Discussions channel**
We use [GitHub Discussions](https://github.com/hoppscotch/hoppscotch/discussions) as the primary channel for anything and everything related to GitHub Externship, ranging from
- Queries on issues youre working or thinking about to take up.
- Mentor advice on your project ideas [Google Drive document once there is enough clarity for easier collaboration and feedback check project proposal ideas above]
- Helping out your friends to onboard and contribute
- Finally even selection updates etc.
Before asking a question please check this [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask)
## **Need help?**
Start a discussion [here](https://github.com/hoppscotch/hoppscotch/discussions) if youre unable to get a response from the [Telegram](https://hoppscotch.io/externship_telegram), [Discord](https://hoppscotch.io/discord) communities.
Start a new idea under [discussions](https://github.com/hoppscotch/hoppscotch/discussions) only once youve finalized your project proposal.
Send us an email at support@hoppscotch.io for any administrative or operational concerns regarding the program.
## **Submit your proposal on time**
You can find the complete GitHub Externship program timeline [here](https://github-externships.github.io/externship/structure.html). Students can begin registering and submitting project proposals to mentor organizations—including Hoppscotch—on April 22, 2021.
All proposals must be submitted by May 21, 2021.
We look forward to seeing your proposals for a happy summer of coding.
## **Mentors**
- [Liyas Thomas](https://github.com/liyasthomas)
- [Andrew Bastin](https://github.com/andrewbastin)
If you have any questions or queries please contact us via [Telegram](https://hoppscotch.io/externship_telegram), [Discord](https://hoppscotch.io/discord) or send an email to us at support@hoppscotch.io.

View File

@@ -6,11 +6,11 @@
<b>Hoppscotch - Open source API development ecosystem</b>
</p>
<p>
<i>Helps you create requests faster, saving precious time on development - <a href="http://eepurl.com/g6n_P5">Subscribe</a></i>
<i>Helps you create requests faster, saving precious time on development - <a href="https://eepurl.com/g6n_P5">Subscribe</a></i>
</p>
<p>
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Travis Build Status](https://img.shields.io/travis/com/hoppscotch/hoppscotch/main?logo=Travis)](https://travis-ci.com/hoppscotch/hoppscotch) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/intent/tweet?url=https%3A%2F%2Fhoppscotch.io&text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20API%20request%20builder%20-%20Helps%20you%20create%20requests%20faster%2C%20saving%20precious%20time%20on%20development&original_referer=https%3A%2F%2Ftwitter.com%2Fshare%3Ftext%3D%25F0%259F%2591%25BD%2520Hoppscotch%2520%25E2%2580%25A2%2520API%2520request%2520builder%2520-%2520Helps%2520you%2520create%2520requests%2520faster%2C%2520saving%2520precious%2520time%2520on%2520development%26url%3Dhttps%3A%2F%2Fhoppscotch.io%26hashtags%3Dhoppscotch%26via%3Dliyasthomas&via=liyasthomas&hashtags=hoppscotch)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Travis Build Status](https://img.shields.io/travis/com/hoppscotch/hoppscotch/main?logo=Travis)](https://travis-ci.com/hoppscotch/hoppscotch) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
</p>
<p>
@@ -45,7 +45,7 @@
#### **Contact**
[![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=Telegram)](https://t.me/hoppscotch) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://discord.gg/GAMWxmR)
[![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=Telegram)](https://hoppscotch.io/telegram) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://hoppscotch.io/discord)
#### **Support**
@@ -410,6 +410,21 @@ _Notes are only available for signed-in users_
</details>
👨‍👩‍👧‍👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
<details>
<summary><i>Features</i></summary>
---
- Unlimited team collections and shared requests
- Unlimited team members
- User roles
---
</details>
**To find out more, please check out [Hoppscotch Wiki](https://github.com/hoppscotch/hoppscotch/wiki).**
## **Demo**
@@ -428,6 +443,7 @@ _Notes are only available for signed-in users_
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Tailwind CSS](https://tailwindcss.com)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org)
- [Nuxt](https://nuxtjs.org)
@@ -548,6 +564,30 @@ Become a financial contributor and help us sustain our community [[Support](#sup
#### GitHub Sponsors
<p align="center">
<a href="https://simplescraper.io/?utm_source=hs" target="_blank" rel="noopener">
<img
width="100"
src="https://raw.githubusercontent.com/hoppscotch/hoppscotch/main/assets/images/Simplescraper.png"
alt="Simplescraper"
/>
</a>
</p>
<p align="center">
<a href="https://github.com/anmolm96" target="_blank" rel="noopener">
<img
width="64"
src="https://github.com/anmolm96.png?size=64"
alt="Anmol Maini"
/>
</a>
<a href="https://github.com/juzhiyuan" target="_blank" rel="noopener">
<img
width="64"
src="https://github.com/juzhiyuan.png?size=64"
alt="琚致远"
/>
</a>
<a href="https://github.com/eldadfux" target="_blank" rel="noopener">
<img
width="64"
@@ -597,13 +637,6 @@ Become a financial contributor and help us sustain our community [[Support](#sup
alt="Erica Brescia"
/>
</a>
<a href="http://tom.preston-werner.com" target="_blank" rel="noopener">
<img
width="64"
src="https://github.com/mojombo.png?size=64"
alt="Tom Preston-Werner"
/>
</a>
<a href="https://github.com/mlynch" target="_blank" rel="noopener">
<img
width="64"
@@ -637,6 +670,13 @@ Become a financial contributor and help us sustain our community [[Support](#sup
#### Open Collective
<p align="center">
<a href="https://pipedream.com/?ref=hoppscotch" target="_blank" rel="noopener">
<img
width="100"
src="https://raw.githubusercontent.com/hoppscotch/hoppscotch/main/assets/images/pipedream.png"
alt="pipedream"
/>
</a>
<a href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=github&utm_campaign=hoppscotch-sponsorship" target="_blank" rel="noopener">
<img
width="100"
@@ -646,11 +686,6 @@ Become a financial contributor and help us sustain our community [[Support](#sup
</a>
</p>
<p align="center">
<a href="https://opencollective.com/hoppscotch/organization/0/website"><img src="https://opencollective.com/hoppscotch/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/hoppscotch/organization/1/website"><img src="https://opencollective.com/hoppscotch/organization/1/avatar.svg"></a>
</p>
### Code Contributors
This project exists thanks to all the people who contribute [[Contribute](CONTRIBUTING.md)].

View File

@@ -1,203 +1,249 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Material_Icons-400-fallback1.woff2') format('woff2');
src: url("~assets/fonts/Material_Icons-400-fallback1.woff2") format("woff2");
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Poppins-400-devanagari2.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
src: url("~assets/fonts/Poppins-400-devanagari2.woff2") format("woff2");
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,
U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Poppins-400-latin-ext3.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Poppins-400-latin-ext3.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Poppins-400-latin4.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Poppins-400-latin4.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('~assets/fonts/Poppins-500-devanagari5.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
src: url("~assets/fonts/Poppins-500-devanagari5.woff2") format("woff2");
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,
U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('~assets/fonts/Poppins-500-latin-ext6.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Poppins-500-latin-ext6.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('~assets/fonts/Poppins-500-latin7.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Poppins-500-latin7.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('~assets/fonts/Poppins-600-devanagari8.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
src: url("~assets/fonts/Poppins-600-devanagari8.woff2") format("woff2");
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,
U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('~assets/fonts/Poppins-600-latin-ext9.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Poppins-600-latin-ext9.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('~assets/fonts/Poppins-600-latin10.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Poppins-600-latin10.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('~assets/fonts/Poppins-700-devanagari11.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
src: url("~assets/fonts/Poppins-700-devanagari11.woff2") format("woff2");
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,
U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('~assets/fonts/Poppins-700-latin-ext12.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Poppins-700-latin-ext12.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('~assets/fonts/Poppins-700-latin13.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Poppins-700-latin13.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('~assets/fonts/Poppins-800-devanagari14.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
src: url("~assets/fonts/Poppins-800-devanagari14.woff2") format("woff2");
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,
U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('~assets/fonts/Poppins-800-latin-ext15.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Poppins-800-latin-ext15.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-family: "Poppins";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('~assets/fonts/Poppins-800-latin16.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Poppins-800-latin16.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-cyrillic-ext17.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
src: url("~assets/fonts/Roboto_Mono-400-cyrillic-ext17.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-cyrillic18.woff2') format('woff2');
src: url("~assets/fonts/Roboto_Mono-400-cyrillic18.woff2") format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-greek19.woff2') format('woff2');
src: url("~assets/fonts/Roboto_Mono-400-greek19.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-vietnamese20.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
src: url("~assets/fonts/Roboto_Mono-400-vietnamese20.woff2") format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-latin-ext21.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url("~assets/fonts/Roboto_Mono-400-latin-ext21.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-family: "Roboto Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-latin22.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
src: url("~assets/fonts/Roboto_Mono-400-latin22.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
.material-icons {
font-family: 'Material Icons';
font-family: "Material Icons"; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */
font-weight: normal;
font-style: normal;
font-size: 24px;
@@ -208,6 +254,6 @@
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
}

View File

@@ -1,5 +1,6 @@
/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
assets/images/pipedream.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -17,7 +17,10 @@ const regex = { ws, sse, socketio }
// type = ws/sse/socketio
async function validator(type, url) {
console.time(`validator ${url}`)
const [res1, res2] = await Promise.all([regex[type][0].test(url), regex[type][1].test(url)])
const [res1, res2] = await Promise.all([
regex[type][0].test(url),
regex[type][1].test(url),
])
console.timeEnd(`validator ${url}`)
return res1 || res2
}

View File

@@ -1,218 +1,12 @@
**Table of Contents**
{{#collections}}
{{>folderContents}}
{{/collections}}
{{#collections}}
# {{name}}
## {{#folders}}
## Folder: {{name}}
{{#requests}}
### {{name}}
**Method**: {{method}}
**Request URL**: `{{{url}}}{{{path}}}`
{{#isHeaders}}
**Headers**:
<table>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#headers}}
<tr>
<td>{{{key}}}</td>
<td>`{{{value}}}`</td>
</tr>
{{/headers}}
</table>
{{/isHeaders}}
{{#isParams}}
**Parameters**:
<table>
<tr>
<th>type</th>
<th>Key</th>
<th>Value</th>
</tr>
{{#params}}
<tr>
<td>{{type}}</td>
<td>{{{key}}}</td>
<td>{{{value}}}</td>
</tr>
{{/params}}
</table>
{{/isParams}}
{{#isAuth}}
**Authentication Type**: {{{auth}}}
{{/isAuth}}
{{#bearerToken}}
**Bearer Token**: `{{{.}}}`
{{/bearerToken}}
{{#isAuthBasic}}
Username: `{{{httpUser}}}`
Password: `{{{httpPassword}}}`
{{/isAuthBasic}}
{{#isRawParams}}
**RawParams**:
```json
{{{rawParams}}}
```
{{/isRawParams}}
{{#contentType}}
**ContentType**: `{{{contentType}}}`
{{/contentType}}
{{#preRequestScript}}
**Pre Request Script**:
```js
{
{
{
preRequestScript
}
}
}
```
{{/preRequestScript}}
{{#testScript}}
**Test Script**:
```js
{
{
{
testScript
}
}
}
```
{{/testScript}}
{{/requests}}
---
{{/folders}}
{{#requests}}
## {{name}}
**Method**: {{method}}
**Request URL**: `{{{url}}}{{{path}}}`
{{#isHeaders}}
**Headers**:
<table>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#headers}}
<tr>
<td>{{{key}}}</td>
<td>`{{{value}}}`</td>
</tr>
{{/headers}}
</table>
{{/isHeaders}}
{{#isParams}}
**Parameters**:
<table>
<tr>
<th>type</th>
<th>Key</th>
<th>Value</th>
</tr>
{{#params}}
<tr>
<td>{{type}}</td>
<td>{{{key}}}</td>
<td>{{{value}}}</td>
</tr>
{{/params}}
</table>
{{/isParams}}
{{#isAuth}}
**Authentication Type**: {{{auth}}}
{{/isAuth}}
{{#bearerToken}}
**Bearer Token**: `{{{.}}}`
{{/bearerToken}}
{{#isAuthBasic}}
Username: `{{{httpUser}}}`
Password: `{{{httpPassword}}}`
{{/isAuthBasic}}
{{#isRawParams}}
**Raw Parameters**:
```json
{{{rawParams}}}
```
{{/isRawParams}}
{{#contentType}}
**Content Type**: `{{{contentType}}}`
{{/contentType}}
{{#preRequestScript}}
**Pre Request Script**:
```js
{
{
{
preRequestScript
}
}
}
```
{{/preRequestScript}}
{{#testScript}}
**Test Script**:
```js
{
{
{
testScript
}
}
}
```
{{/testScript}}
{{/requests}}
{{>folderBody}}
{{/collections}}

113
assets/md/folderBody.md Normal file
View File

@@ -0,0 +1,113 @@
{{nestingLevel}} {{name}}
{{#requests}}
{{nestingLevel}} Request: {{name}}
**Method**: {{method}}
**Request URL**:
```
{{{url}}}{{{path}}}
```
{{#isHeaders}}
**Headers**:
<table>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#headers}}
<tr>
<td>{{{key}}}</td>
<td>`{{{value}}}`</td>
</tr>
{{/headers}}
</table>
{{/isHeaders}}
{{#isParams}}
**Parameters**:
<table>
<tr>
<th>type</th>
<th>Key</th>
<th>Value</th>
</tr>
{{#params}}
<tr>
<td>{{type}}</td>
<td>{{{key}}}</td>
<td>{{{value}}}</td>
</tr>
{{/params}}
</table>
{{/isParams}}
{{#isAuth}}
**Authentication Type**: {{{auth}}}
{{/isAuth}}
{{#bearerToken}}
**Bearer Token**: `{{{.}}}`
{{/bearerToken}}
{{#isAuthBasic}}
Username: `{{{httpUser}}}`
Password: `{{{httpPassword}}}`
{{/isAuthBasic}}
{{#isRawParams}}
**Raw Parameters**:
```json
{{{rawParams}}}
```
{{/isRawParams}}
{{#contentType}}
**Content Type**: `{{{contentType}}}`
{{/contentType}}
{{#isPreRequestScript}}
**Pre Request Script**:
```js
{
{
{
preRequestScript
}
}
}
```
{{/isPreRequestScript}}
{{#isTestScript}}
**Test Script**:
```js
{
{
{
testScript
}
}
}
```
{{/isTestScript}}
{{/requests}}
{{#folders}}
{{> folderBody }}
{{/folders}}

View File

@@ -0,0 +1,4 @@
{{{id}}} <a href="#{{ref}}"> {{name}} </a> <br>
{{#folders}}
{{> folderContents}}
{{/folders}}

View File

@@ -5,7 +5,11 @@ function isBabelLoader(caller) {
module.exports = function (api) {
if (api.env("test") && !api.caller(isBabelLoader)) {
return {
plugins: ["@babel/plugin-proposal-class-properties"],
plugins: [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-optional-chaining",
],
presets: [
[
"@babel/preset-env",

View File

@@ -9,10 +9,10 @@
<div class="px-2 row-wrapper">
<span>
<a
v-tooltip.right="$t('recurring')"
href="https://github.com/sponsors/hoppscotch"
target="_blank"
rel="noopener"
v-tooltip.right="$t('recurring')"
>
<button class="icon">
<i class="material-icons">
@@ -36,10 +36,10 @@
<div class="px-2 row-wrapper">
<span>
<a
v-tooltip.right="$t('one_time_recurring')"
href="https://opencollective.com/hoppscotch"
target="_blank"
rel="noopener"
v-tooltip.right="$t('one_time_recurring')"
>
<button class="icon">
<i class="material-icons">donut_large</i>
@@ -51,10 +51,10 @@
<div class="px-2 row-wrapper">
<span>
<a
v-tooltip.right="$t('recurring')"
href="https://www.patreon.com/liyasthomas"
target="_blank"
rel="noopener"
v-tooltip.right="$t('recurring')"
>
<button class="icon">
<i class="material-icons">local_parking</i>
@@ -66,10 +66,10 @@
<div class="px-2 row-wrapper">
<span>
<a
v-tooltip.right="$t('one_time')"
href="https://www.paypal.me/liyascthomas"
target="_blank"
rel="noopener"
v-tooltip.right="$t('one_time')"
>
<button class="icon">
<i class="material-icons">payment</i>
@@ -82,15 +82,17 @@
<div class="p-2">
<h3 class="title">Financial Contributors</h3>
<div class="contributors">
<a href="https://oss.capital/?ref=hoppscotch" target="_blank" rel="noopener">
<a
href="https://oss.capital/?ref=hoppscotch"
target="_blank"
rel="noopener"
>
<img
style="height: 100%; width: 300px"
src="~assets/images/ossc-banner.svg"
alt="OSS Capital"
/>
</a>
</div>
<div class="contributors">
<a
href="https://appwrite.io/?utm_source=hoppscotch&utm_medium=banner&utm_campaign=hello"
target="_blank"
@@ -102,8 +104,17 @@
alt="Appwrite"
/>
</a>
</div>
<div class="contributors">
<a
href="https://pipedream.com/?ref=hoppscotch"
target="_blank"
rel="noopener"
>
<img
style="max-width: 100px"
src="~assets/images/pipedream.png"
alt="pipedream"
/>
</a>
<a
href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=website&utm_campaign=hoppscotch-sponsorship"
target="_blank"
@@ -115,8 +126,17 @@
alt="Paw"
/>
</a>
</div>
<div class="contributors">
<a
href="https://simplescraper.io/?utm_source=hs"
target="_blank"
rel="noopener"
>
<img
style="max-height: 50px"
src="~assets/images/Simplescraper_dark.png"
alt="Simplescraper"
/>
</a>
<a href="https://tyk.io?ref=hoppscotch" target="_blank" rel="noopener">
<img
style="max-width: 320px"
@@ -125,22 +145,6 @@
/>
</a>
</div>
<div class="contributors">
<a
target="_blank"
rel="noopener"
href="https://opencollective.com/hoppscotch/organization/0/website"
>
<img src="https://opencollective.com/hoppscotch/organization/0/avatar.svg" />
</a>
<a
target="_blank"
rel="noopener"
href="https://opencollective.com/hoppscotch/organization/1/website"
>
<img src="https://opencollective.com/hoppscotch/organization/1/avatar.svg" />
</a>
</div>
</div>
<hr />
<p class="info">
@@ -162,8 +166,10 @@
.contributors {
@apply flex;
@apply items-center;
@apply flex-nowrap;
@apply flex-wrap;
@apply overflow-auto;
@apply m-2;
@apply space-x-2;
@apply space-y-2;
}
</style>

View File

@@ -21,13 +21,22 @@
rel="noopener"
>
<button class="icon">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
>
<path
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm8.003 8.657c-1.276-3.321-4.46-4.605-5.534-4.537 3.529 1.376 4.373 6.059 4.06 7.441-.307-1.621-1.286-3.017-1.872-3.385 3.417 8.005-4.835 10.465-7.353 7.687.649.168 1.931.085 2.891-.557.898-.602.983-.638 1.56-.683.686-.053-.041-1.406-1.539-1.177-.616.094-1.632.819-2.88.341-1.508-.576-1.46-2.634.096-2.015.337-.437.088-1.263.088-1.263.452-.414 1.022-.706 1.37-.911.228-.135.829-.507.795-1.23-.123-.096-.32-.219-.766-.193-1.736.11-1.852-.518-1.967-.808.078-.668.524-1.534 1.361-1.931-1.257-.193-2.28.397-2.789 1.154-.809-.174-1.305-.183-2.118-.031-.316-.24-.666-.67-.878-1.181C6.36 3.312 9.027 2 12 2c5.912 0 8.263 4.283 8.003 6.657z"
/>
</svg>
<span>Firefox</span>
<span class="icon" v-if="hasFirefoxExtInstalled" v-tooltip="$t('installed')">
<span
v-if="hasFirefoxExtInstalled"
v-tooltip="$t('installed')"
class="icon"
>
<i class="material-icons">done</i>
</span>
</button>
@@ -40,13 +49,22 @@
rel="noopener"
>
<button class="icon">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
>
<path
d="M2.897 4.181A11.87 11.87 0 0111.969 0c4.288 0 8.535 2.273 10.717 6.554h-9.293c-1.674.001-2.755-.037-3.926.579-1.376.724-2.415 2.067-2.777 3.644L2.897 4.181zM8.007 12c0 2.2 1.789 3.99 3.988 3.99s3.988-1.79 3.988-3.99-1.789-3.991-3.988-3.991S8.007 9.8 8.007 12zm5.536 5.223c-2.238.666-4.858-.073-6.293-2.549-1.095-1.891-3.989-6.933-5.305-9.225A11.856 11.856 0 000 11.956c0 5.448 3.726 10.65 9.673 11.818l3.87-6.551zm2.158-9.214a5.463 5.463 0 011.007 6.719 1815.43 1815.43 0 01-5.46 9.248C18.437 24.419 24 18.616 24 12.004c0-1.313-.22-2.66-.69-3.995h-7.609z"
/>
</svg>
<span>Chrome</span>
<span class="icon" v-if="hasChromeExtInstalled" v-tooltip="$t('installed')">
<span
v-if="hasChromeExtInstalled"
v-tooltip="$t('installed')"
class="icon"
>
<i class="material-icons">done</i>
</span>
</button>
@@ -67,18 +85,18 @@ export default {
props: {
show: Boolean,
},
watch: {
show() {
this.hasChromeExtInstalled = hasChromeExtensionInstalled()
this.hasFirefoxExtInstalled = hasFirefoxExtensionInstalled()
},
},
data() {
return {
hasChromeExtInstalled: hasChromeExtensionInstalled(),
hasFirefoxExtInstalled: hasFirefoxExtensionInstalled(),
}
},
watch: {
show() {
this.hasChromeExtInstalled = hasChromeExtensionInstalled()
this.hasFirefoxExtInstalled = hasFirefoxExtensionInstalled()
},
},
methods: {
hideModal() {
this.$emit("hide-modal")

View File

@@ -1,11 +1,18 @@
<template>
<footer class="footer">
<div class="row-wrapper">
<span class="flex flex-col font-mono md:flex-row" style="align-items: start">
<a class="footer-link" href="https://www.netlify.com" target="_blank" rel="noopener">
<span
class="flex flex-col font-mono md:flex-row"
style="align-items: start"
>
<a
class="footer-link"
href="https://www.netlify.com"
target="_blank"
rel="noopener"
>
Powered by Netlify
</a>
<span class="footer-link"> Sponsored by </span>
<span>
<a
class="footer-link"
@@ -15,9 +22,6 @@
>
OSS Capital
</a>
</span>
<span class="footer-link"> & </span>
<span>
<a
class="footer-link"
href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=website&utm_campaign=hoppscotch-sponsorship"
@@ -26,6 +30,14 @@
>
Paw
</a>
<a
class="footer-link"
href="https://simplescraper.io/?utm_source=hs"
target="_blank"
rel="noopener"
>
Simplescraper
</a>
</span>
<iframe
src="https://ghbtns.com/github-btn.html?user=hoppscotch&type=sponsor"
@@ -38,20 +50,23 @@
loading="lazy"
></iframe>
</span>
<span class="flex flex-col font-mono md:flex-row" style="align-items: start">
<a href="mailto:liyascthomas@gmail.com" target="_blank" rel="noopener">
<button class="icon" v-tooltip="$t('contact_us')">
<span
class="flex flex-col font-mono md:flex-row"
style="align-items: start"
>
<a href="mailto:support@hoppscotch.io" target="_blank" rel="noopener">
<button v-tooltip="$t('contact_us')" class="icon">
<i class="material-icons">email</i>
</button>
</a>
<v-popover>
<button class="icon" v-tooltip="$t('choose_language')">
<button v-tooltip="$t('choose_language')" class="icon">
<i class="material-icons">translate</i>
</button>
<template slot="popover">
<div v-for="locale in availableLocales" :key="locale.code">
<nuxt-link :to="switchLocalePath(locale.code)">
<button class="icon" v-close-popover>
<button v-close-popover class="icon">
{{ locale.name }}
</button>
</nuxt-link>
@@ -63,20 +78,6 @@
</footer>
</template>
<style scoped lang="scss">
.footer-link {
@apply inline-flex;
@apply flex-shrink-0;
@apply my-2;
@apply mx-4;
@apply text-fgLightColor;
&:hover {
@apply text-fgColor;
}
}
</style>
<script>
export default {
computed: {
@@ -86,3 +87,18 @@ export default {
},
}
</script>
<style scoped lang="scss">
.footer-link {
@apply inline-flex;
@apply flex-shrink-0;
@apply my-2;
@apply mx-4;
@apply text-fgLightColor;
@apply text-sm;
&:hover {
@apply text-fgColor;
}
}
</style>

View File

@@ -22,16 +22,31 @@
href="https://appwrite.io/?utm_source=hoppscotch&utm_medium=banner&utm_campaign=hello"
target="_blank"
rel="noopener"
class="inline-flex items-center px-4 py-2 mx-4 font-mono text-sm rounded-md bg-bgDarkColor hide-on-small-screen"
class="
inline-flex
items-center
px-4
py-2
mx-4
font-mono
text-sm
rounded-md
bg-bgDarkColor
hide-on-small-screen
"
>
Appwrite - Open-Source Backend as a Service
<img class="w-8 ml-2" src="~assets/images/appwrite-icon.svg" alt="Appwrite" />
<img
class="w-8 ml-2"
src="~assets/images/appwrite-icon.svg"
alt="Appwrite"
/>
</a>
<button
class="icon"
id="installPWA"
@click.prevent="showInstallPrompt()"
v-tooltip="$t('install_pwa')"
class="icon"
@click.prevent="showInstallPrompt()"
>
<i class="material-icons">offline_bolt</i>
</button>
@@ -41,8 +56,13 @@
aria-label="GitHub"
rel="noopener"
>
<button class="icon" aria-label="GitHub" v-tooltip="'GitHub'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons">
<button v-tooltip="'GitHub'" class="icon" aria-label="GitHub">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path
d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"
/>
@@ -50,7 +70,7 @@
</button>
</a>
<v-popover v-if="fb.currentUser === null">
<button class="icon" v-tooltip="$t('login_with')">
<button v-tooltip="$t('login_with')" class="icon">
<i class="material-icons">login</i>
</button>
<template slot="popover">
@@ -59,12 +79,13 @@
</v-popover>
<v-popover v-else>
<button
class="icon"
v-tooltip="
(fb.currentUser.displayName || '<label><i>Name not found</i></label>') +
(fb.currentUser.displayName ||
'<label><i>Name not found</i></label>') +
'<br>' +
(fb.currentUser.email || '<label><i>Email not found</i></label>')
"
class="icon"
aria-label="Account"
>
<img
@@ -77,7 +98,7 @@
</button>
<template slot="popover">
<div>
<nuxt-link :to="localePath('settings')" v-close-popover>
<nuxt-link v-close-popover :to="localePath('settings')">
<button class="icon">
<i class="material-icons">settings</i>
<span>
@@ -92,26 +113,26 @@
</template>
</v-popover>
<v-popover>
<button class="icon" v-tooltip="$t('more')">
<button v-tooltip="$t('more')" class="icon">
<i class="material-icons">drag_indicator</i>
</button>
<template slot="popover">
<button class="icon" @click="showExtensions = true" v-close-popover>
<button v-close-popover class="icon" @click="showExtensions = true">
<i class="material-icons">extension</i>
<span>{{ $t("extensions") }}</span>
</button>
<button class="icon" @click="showShortcuts = true" v-close-popover>
<button v-close-popover class="icon" @click="showShortcuts = true">
<i class="material-icons">keyboard</i>
<span>{{ $t("shortcuts") }}</span>
</button>
<button class="icon" @click="showSupport = true" v-close-popover>
<button v-close-popover class="icon" @click="showSupport = true">
<i class="material-icons">favorite</i>
<span>{{ $t("support_us") }}</span>
</button>
<button
class="icon"
onClick="window.open('https://twitter.com/share?text=👽 Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=liyasthomas');"
v-close-popover
class="icon"
onClick="window.open('https://twitter.com/share?text=👽 Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io');"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path
@@ -122,10 +143,10 @@
</button>
<button
v-if="navigatorShare"
class="icon"
@click="nativeShare"
v-close-popover
v-tooltip="$t('more')"
class="icon"
@click="nativeShare"
>
<i class="material-icons">share</i>
<span>Share</span>
@@ -134,12 +155,149 @@
</v-popover>
</span>
</div>
<AppExtensions :show="showExtensions" @hide-modal="showExtensions = false" />
<AppExtensions
:show="showExtensions"
@hide-modal="showExtensions = false"
/>
<AppShortcuts :show="showShortcuts" @hide-modal="showShortcuts = false" />
<AppSupport :show="showSupport" @hide-modal="showSupport = false" />
</header>
</template>
<script>
import intializePwa from "~/helpers/pwa"
import { fb } from "~/helpers/fb"
// import { hasExtensionInstalled } from "~/helpers/strategies/ExtensionStrategy"
export default {
data() {
return {
// Once the PWA code is initialized, this holds a method
// that can be called to show the user the installation
// prompt.
showInstallPrompt: null,
showExtensions: false,
showShortcuts: false,
showSupport: false,
navigatorShare: navigator.share,
fb,
}
},
async mounted() {
this._keyListener = function (e) {
if (e.key === "Escape") {
e.preventDefault()
this.showExtensions = this.showShortcuts = this.showSupport = false
}
}
document.addEventListener("keydown", this._keyListener.bind(this))
// Initializes the PWA code - checks if the app is installed,
// etc.
this.showInstallPrompt = await intializePwa()
const cookiesAllowed = localStorage.getItem("cookiesAllowed") === "yes"
if (!cookiesAllowed) {
this.$toast.show(this.$t("we_use_cookies"), {
icon: "info",
duration: 5000,
theme: "toasted-primary",
action: [
{
text: this.$t("dismiss"),
onClick: (_, toastObject) => {
localStorage.setItem("cookiesAllowed", "yes")
toastObject.goAway(0)
},
},
],
})
}
// let showAd = localStorage.getItem("showAd") === "no"
// if (!showAd) {
// setTimeout(() => {
// this.$toast.clear()
// this.$toast.show(
// "<span><a href='https://github.com/sponsors/hoppscotch' target='_blank' rel='noopener'>Sponsor us to support Hoppscotch open source project 💖</a><br><sub>Whoosh this away to dismiss.</sub></span>",
// {
// icon: "",
// duration: 0,
// theme: "toasted-ad",
// action: [
// {
// text: "Sponsor",
// icon: "chevron_right",
// onClick: (_, toastObject) => {
// localStorage.setItem("showAd", "no")
// toastObject.goAway(0)
// window.open("https://github.com/sponsors/hoppscotch")
// },
// },
// ],
// onComplete() {
// localStorage.setItem("showAd", "no")
// },
// }
// )
// }, 8000)
// }
// let showExtensionsToast = localStorage.getItem("showExtensionsToast") === "yes"
// if (!showExtensionsToast) {
// setTimeout(() => {
// if (!hasExtensionInstalled()) {
// this.$toast.show(this.$t("extensions_info2"), {
// icon: "extension",
// duration: 5000,
// theme: "toasted-primary",
// action: [
// {
// text: this.$t("yes"),
// onClick: (_, toastObject) => {
// this.showExtensions = true
// localStorage.setItem("showExtensionsToast", "yes")
// toastObject.goAway(0)
// },
// },
// {
// text: this.$t("no"),
// onClick: (_, toastObject) => {
// this.$store.commit("setMiscState", {
// value: false,
// attribute: "showExtensionsToast",
// })
// localStorage.setItem("showExtensionsToast", "no")
// toastObject.goAway(0)
// },
// },
// ],
// })
// }
// }, 5000)
// }
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
methods: {
nativeShare() {
if (navigator.share) {
navigator
.share({
title: "Hoppscotch",
text: "Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.",
url: "https://hoppscotch.io",
})
.then(() => {})
.catch(console.error)
} else {
// fallback
}
},
},
}
</script>
<style scoped lang="scss">
$responsiveWidth: 768px;
@@ -182,138 +340,3 @@ $responsiveWidth: 768px;
}
}
</style>
<script>
import intializePwa from "~/helpers/pwa"
import { fb } from "~/helpers/fb"
// import { hasExtensionInstalled } from "~/helpers/strategies/ExtensionStrategy"
export default {
data() {
return {
// Once the PWA code is initialized, this holds a method
// that can be called to show the user the installation
// prompt.
showInstallPrompt: null,
showExtensions: false,
showShortcuts: false,
showSupport: false,
navigatorShare: navigator.share,
fb,
}
},
async mounted() {
this._keyListener = function (e) {
if (e.key === "Escape") {
e.preventDefault()
this.showExtensions = this.showShortcuts = this.showSupport = false
}
}
document.addEventListener("keydown", this._keyListener.bind(this))
// Initializes the PWA code - checks if the app is installed,
// etc.
this.showInstallPrompt = await intializePwa()
let cookiesAllowed = localStorage.getItem("cookiesAllowed") === "yes"
if (!cookiesAllowed) {
this.$toast.show(this.$t("we_use_cookies"), {
icon: "info",
duration: 5000,
theme: "toasted-primary",
action: [
{
text: this.$t("dismiss"),
onClick: (e, toastObject) => {
localStorage.setItem("cookiesAllowed", "yes")
toastObject.goAway(0)
},
},
],
})
}
// let showAd = localStorage.getItem("showAd") === "no"
// if (!showAd) {
// setTimeout(() => {
// this.$toast.clear()
// this.$toast.show(
// "<span><a href='https://github.com/sponsors/hoppscotch' target='_blank' rel='noopener'>Sponsor us to support Hoppscotch open source project 💖</a><br><sub>Whoosh this away to dismiss.</sub></span>",
// {
// icon: "",
// duration: 0,
// theme: "toasted-ad",
// action: [
// {
// text: "Sponsor",
// icon: "chevron_right",
// onClick: (e, toastObject) => {
// localStorage.setItem("showAd", "no")
// toastObject.goAway(0)
// window.open("https://github.com/sponsors/hoppscotch")
// },
// },
// ],
// onComplete() {
// localStorage.setItem("showAd", "no")
// },
// }
// )
// }, 8000)
// }
// let showExtensionsToast = localStorage.getItem("showExtensionsToast") === "yes"
// if (!showExtensionsToast) {
// setTimeout(() => {
// if (!hasExtensionInstalled()) {
// this.$toast.show(this.$t("extensions_info2"), {
// icon: "extension",
// duration: 5000,
// theme: "toasted-primary",
// action: [
// {
// text: this.$t("yes"),
// onClick: (e, toastObject) => {
// this.showExtensions = true
// localStorage.setItem("showExtensionsToast", "yes")
// toastObject.goAway(0)
// },
// },
// {
// text: this.$t("no"),
// onClick: (e, toastObject) => {
// this.$store.commit("setMiscState", {
// value: false,
// attribute: "showExtensionsToast",
// })
// localStorage.setItem("showExtensionsToast", "no")
// toastObject.goAway(0)
// },
// },
// ],
// })
// }
// }, 5000)
// }
},
methods: {
nativeShare() {
if (navigator.share) {
navigator
.share({
title: "Hoppscotch",
text:
"Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.",
url: "https://hoppscotch.io",
})
.then(() => {})
.catch(console.error)
} else {
// fallback
}
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
}
</script>

View File

@@ -15,6 +15,7 @@ export default {
props: {
color: {
type: String,
default: "",
},
},
}

View File

@@ -6,12 +6,51 @@
{{ isCollapsed(label) ? "expand_more" : "expand_less" }}
</i>
</legend>
<div class="collapsible" :class="{ hidden: isCollapsed(label.toLowerCase()) }">
<slot />
<div
class="collapsible"
:class="{ hidden: isCollapsed(label.toLowerCase()) }"
>
<slot></slot>
</div>
</fieldset>
</template>
<script lang="ts">
import Vue from "vue"
export default Vue.extend({
props: {
label: {
type: String,
default: "Section",
},
noLegend: {
type: Boolean,
default: false,
},
},
computed: {
sectionString(): string {
return `${this.$route.path.replace(/\/+$/, "")}/${this.label}`
},
},
methods: {
collapse() {
// Save collapsed section into the collapsedSections array
this.$store.commit("setCollapsedSection", this.sectionString)
},
isCollapsed(_label: string) {
return (
this.$store.state.theme.collapsedSections.includes(
this.sectionString
) || false
)
},
},
})
</script>
<style scoped lang="scss">
fieldset {
@apply my-4;
@@ -33,35 +72,3 @@ fieldset {
}
}
</style>
<script lang="ts">
import Vue from "vue"
export default Vue.extend({
computed: {
sectionString(): string {
return `${this.$route.path.replace(/\/+$/, "")}/${this.label}`
},
},
props: {
label: {
type: String,
default: "Section",
},
noLegend: {
type: Boolean,
default: false,
},
},
methods: {
collapse() {
// Save collapsed section into the collapsedSections array
this.$store.commit("setCollapsedSection", this.sectionString)
},
isCollapsed(_label: string) {
return this.$store.state.theme.collapsedSections.includes(this.sectionString) || false
},
},
})
</script>

View File

@@ -73,18 +73,6 @@
</SmartModal>
</template>
<style scoped lang="scss">
kbd {
@apply inline-flex;
@apply resize-none;
@apply m-2;
@apply rounded-lg;
@apply py-2;
@apply px-4;
@apply text-sm;
}
</style>
<script>
import { getPlatformSpecialKey } from "~/helpers/platformutils"
@@ -100,3 +88,15 @@ export default {
},
}
</script>
<style scoped lang="scss">
kbd {
@apply inline-flex;
@apply resize-none;
@apply m-2;
@apply rounded-lg;
@apply py-2;
@apply px-4;
@apply text-sm;
}
</style>

View File

@@ -6,27 +6,32 @@
seems to mess up the nuxt-link active class.
-->
<nuxt-link
v-tooltip.right="$t('home')"
:to="localePath('index')"
:class="linkActive('/')"
v-tooltip.right="$t('home')"
:aria-label="$t('home')"
>
<AppLogo alt class="material-icons" style="height: 24px" />
</nuxt-link>
<nuxt-link
v-tooltip.right="$t('realtime')"
:to="localePath('realtime')"
:class="linkActive('/realtime')"
v-tooltip.right="$t('realtime')"
>
<i class="material-icons">language</i>
</nuxt-link>
<nuxt-link
v-tooltip.right="$t('graphql')"
:to="localePath('graphql')"
:class="linkActive('/graphql')"
v-tooltip.right="$t('graphql')"
:aria-label="$t('graphql')"
>
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 29.999 30">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
width="24"
viewBox="0 0 29.999 30"
>
<path d="M4.08 22.864l-1.1-.636L15.248.98l1.1.636z" />
<path d="M2.727 20.53h24.538v1.272H2.727z" />
<path
@@ -43,80 +48,111 @@
</svg>
</nuxt-link>
<nuxt-link
v-tooltip.right="$t('documentation')"
:to="localePath('doc')"
:class="linkActive('/doc')"
v-tooltip.right="$t('documentation')"
:aria-label="$t('documentation')"
>
<i class="material-icons">topic</i>
</nuxt-link>
<nuxt-link
v-tooltip.right="$t('settings')"
:to="localePath('settings')"
:class="linkActive('/settings')"
v-tooltip.right="$t('settings')"
:aria-label="$t('settings')"
>
<i class="material-icons">settings</i>
</nuxt-link>
</nav>
<nav v-if="$route.path == '/'" class="secondary-nav">
<a href="#request" v-tooltip.right="$t('request')">
<a v-tooltip.right="$t('request')" href="#request">
<i class="material-icons">cloud_upload</i>
</a>
<a href="#options" v-tooltip.right="$t('options')">
<a v-tooltip.right="$t('options')" href="#options">
<i class="material-icons">toc</i>
</a>
<a href="#response" v-tooltip.right="$t('response')">
<a v-tooltip.right="$t('response')" href="#response">
<i class="material-icons">cloud_download</i>
</a>
</nav>
<nav v-else-if="$route.path.includes('/realtime')" class="secondary-nav">
<a href="#request" v-tooltip.right="$t('request')">
<a v-tooltip.right="$t('request')" href="#request">
<i class="material-icons">cloud_upload</i>
</a>
<a href="#response" v-tooltip.right="$t('communication')">
<a v-tooltip.right="$t('communication')" href="#response">
<i class="material-icons">cloud_download</i>
</a>
</nav>
<nav v-else-if="$route.path.includes('/graphql')" class="secondary-nav">
<a href="#endpoint" v-tooltip.right="$t('endpoint')">
<a v-tooltip.right="$t('endpoint')" href="#endpoint">
<i class="material-icons">cloud</i>
</a>
<a href="#schema" v-tooltip.right="$t('schema')">
<a v-tooltip.right="$t('schema')" href="#schema">
<i class="material-icons">assignment_returned</i>
</a>
<a href="#query" v-tooltip.right="$t('query')">
<a v-tooltip.right="$t('query')" href="#query">
<i class="material-icons">cloud_upload</i>
</a>
<a href="#response" v-tooltip.right="$t('response')">
<a v-tooltip.right="$t('response')" href="#response">
<i class="material-icons">cloud_download</i>
</a>
</nav>
<nav v-else-if="$route.path.includes('/doc')" class="secondary-nav">
<a href="#import" v-tooltip.right="$t('import')">
<a v-tooltip.right="$t('import')" href="#import">
<i class="material-icons">folder</i>
</a>
<a href="#documentation" v-tooltip.right="$t('documentation')">
<a v-tooltip.right="$t('documentation')" href="#documentation">
<i class="material-icons">insert_drive_file</i>
</a>
</nav>
<nav v-else-if="$route.path.includes('/settings')" class="secondary-nav">
<a href="#account" v-tooltip.right="$t('account')">
<a v-tooltip.right="$t('account')" href="#account">
<i class="material-icons">person</i>
</a>
<a href="#theme" v-tooltip.right="$t('theme')">
<a v-tooltip.right="$t('theme')" href="#theme">
<i class="material-icons">brush</i>
</a>
<a href="#extensions" v-tooltip.right="$t('extensions')">
<a v-tooltip.right="$t('extensions')" href="#extensions">
<i class="material-icons">extension</i>
</a>
<a href="#proxy" v-tooltip.right="$t('proxy')">
<a v-tooltip.right="$t('proxy')" href="#proxy">
<i class="material-icons">public</i>
</a>
</nav>
</aside>
</template>
<script>
export default {
mounted() {
window.addEventListener("scroll", () => {
const mainNavLinks = document.querySelectorAll("nav.secondary-nav a")
const fromTop = window.scrollY
mainNavLinks.forEach(({ hash, classList }) => {
const section = document.querySelector(hash)
if (
section &&
section.offsetTop <= fromTop &&
section.offsetTop + section.offsetHeight > fromTop
) {
classList.add("current")
} else {
classList.remove("current")
}
})
})
},
methods: {
linkActive(path) {
return {
"nuxt-link-exact-active": this.$route.path === path,
"nuxt-link-active": this.$route.path === path,
}
},
},
}
</script>
<style scoped lang="scss">
$responsiveWidth: 768px;
@@ -269,39 +305,3 @@ nav.secondary-nav {
}
}
</style>
<script>
export default {
methods: {
linkActive(path) {
return {
"nuxt-link-exact-active": this.$route.path === path,
"nuxt-link-active": this.$route.path === path,
}
},
},
mounted() {
window.addEventListener("scroll", (event) => {
let mainNavLinks = document.querySelectorAll("nav ul li a")
let fromTop = window.scrollY
mainNavLinks.forEach(({ hash, classList }) => {
let section = document.querySelector(hash)
if (
section &&
section.offsetTop <= fromTop &&
section.offsetTop + section.offsetHeight > fromTop
) {
classList.add("current")
} else {
classList.remove("current")
}
})
})
},
// watch: {
// $route() {
// // this.$toast.clear();
// },
// },
}
</script>

View File

@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="$t('my_new_collection')"
@keyup.enter="addNewCollection"
/>
@@ -37,47 +37,23 @@
</template>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
},
data() {
return {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
name: null,
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
addNewCollection() {
if (!this.$data.name) {
this.$toast.info(this.$t("invalid_collection_name"))
return
}
this.$store.commit("postwoman/addNewCollection", {
name: this.$data.name,
flag: "rest",
})
this.$emit("hide-modal")
this.syncCollections()
this.$emit("submit", this.name)
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
this.$data.name = undefined
},
},
}

View File

@@ -1,5 +1,5 @@
<template>
<SmartModal v-if="show" @close="show = false">
<SmartModal v-if="show" @close="$emit('hide-modal')">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("new_folder") }}</h3>
@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="$t('my_new_folder')"
@keyup.enter="addFolder"
/>
@@ -40,13 +40,13 @@
export default {
props: {
show: Boolean,
folder: Object,
folderPath: String,
collectionIndex: Number,
folder: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
},
data() {
return {
name: undefined,
name: null,
}
},
methods: {
@@ -56,8 +56,10 @@ export default {
folder: this.folder,
path: this.folderPath || `${this.collectionIndex}`,
})
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -0,0 +1,105 @@
<template>
<div v-if="show">
<SmartTabs
:id="'collections_tab'"
styles="m-4"
@tab-changed="updateCollectionsType"
>
<SmartTab
:id="'my-collections'"
:label="'My Collections'"
:selected="true"
/>
<SmartTab
v-if="currentUser && currentUser.eaInvited && !doc"
:id="'team-collections'"
:label="'Team Collections'"
>
<ul>
<li>
<span class="select-wrapper">
<SmartIntersection @intersecting="onTeamSelectIntersect">
<select
id="team"
type="text"
class="team"
autofocus
@change="updateSelectedTeam(myTeams[$event.target.value])"
>
<option
:key="undefined"
:value="undefined"
hidden
disabled
selected
>
Select team
</option>
<option
v-for="(team, index) in myTeams"
:key="index"
:value="index"
>
{{ team.name }}
</option>
</select>
</SmartIntersection>
</span>
</li>
</ul>
</SmartTab>
</SmartTabs>
</div>
</template>
<script>
import gql from "graphql-tag"
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
export default {
props: {
doc: Boolean,
show: Boolean,
},
data() {
return {
skipTeamsFetching: true,
}
},
subscriptions() {
return {
currentUser: currentUserInfo$,
}
},
apollo: {
myTeams: {
query: gql`
query GetMyTeams {
myTeams {
id
name
myRole
}
}
`,
pollInterval: 10000,
skip() {
return this.skipTeamsFetching
},
},
},
methods: {
onTeamSelectIntersect() {
// Load team data as soon as intersection
this.$apollo.queries.myTeams.refetch()
this.skipTeamsFetching = false
},
updateCollectionsType(tabID) {
this.$emit("update-collection-type", tabID)
},
updateSelectedTeam(team) {
this.$emit("update-selected-team", team)
},
},
}
</script>

View File

@@ -13,10 +13,10 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
:placeholder="editingCollection.name"
type="text"
:placeholder="placeholderCollName"
@keyup.enter="saveCollection"
/>
</div>
@@ -37,52 +37,23 @@
</template>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
editingCollection: Object,
editingCollectionIndex: Number,
placeholderCollName: { type: String, default: null },
},
data() {
return {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
name: null,
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
saveCollection() {
if (!this.$data.name) {
this.$toast.info(this.$t("invalid_collection_name"))
return
}
const collectionUpdated = {
...this.$props.editingCollection,
name: this.$data.name,
}
this.$store.commit("postwoman/editCollection", {
collection: collectionUpdated,
collectionIndex: this.$props.editingCollectionIndex,
flag: "rest",
})
this.$emit("hide-modal")
this.syncCollections()
this.$emit("submit", this.name)
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -1,5 +1,5 @@
<template>
<SmartModal v-if="show" @close="show = false">
<SmartModal v-if="show" @close="$emit('hide-modal')">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("edit_folder") }}</h3>
@@ -13,10 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
:placeholder="folder.name"
type="text"
@keyup.enter="editFolder"
/>
</div>
@@ -37,47 +36,22 @@
</template>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
collectionIndex: Number,
folder: Object,
folderIndex: Number,
},
data() {
return {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
name: null,
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
editFolder() {
this.$store.commit("postwoman/editFolder", {
collectionIndex: this.$props.collectionIndex,
folder: { ...this.$props.folder, name: this.$data.name },
folderIndex: this.$props.folderIndex,
folderName: this.$props.folder.name,
flag: "rest",
})
this.$emit("submit", this.name)
this.hideModal()
this.syncCollections()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -13,11 +13,11 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="requestUpdateData.name"
type="text"
:placeholder="placeholderReqName"
@keyup.enter="saveRequest"
:placeholder="request.name"
/>
</div>
<div slot="footer">
@@ -37,58 +37,25 @@
</template>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
collectionIndex: Number,
folderIndex: Number,
folderName: String,
request: Object,
requestIndex: Number,
placeholderReqName: { type: String, default: null },
},
data() {
return {
requestUpdateData: {
name: undefined,
name: null,
},
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
saveRequest() {
const requestUpdated = {
...this.$props.request,
name: this.$data.requestUpdateData.name || this.$props.request.name,
}
this.$store.commit("postwoman/editRequest", {
requestCollectionIndex: this.$props.collectionIndex,
requestFolderName: this.$props.folderName,
requestFolderIndex: this.$props.folderIndex,
requestNew: requestUpdated,
requestIndex: this.$props.requestIndex,
flag: "rest",
})
this.$emit("submit", this.requestUpdateData)
this.hideModal()
this.syncCollections()
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},

View File

@@ -2,15 +2,32 @@
<SmartModal v-if="show" @close="hideModal">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("import_export") }} {{ $t("collections") }}</h3>
<h3 class="title">Export</h3>
<div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button
v-if="mode != 'import_export'"
v-tooltip.left="'Back'"
class="tooltip-target icon"
@click="mode = 'import_export'"
>
<i class="material-icons">arrow_back</i>
</button>
<v-popover
v-if="
mode == 'import_export' &&
collectionsType.type == 'my-collections'
"
>
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="readCollectionGist" v-close-popover>
<button
v-close-popover
class="icon"
@click="readCollectionGist"
>
<i class="material-icons">assignment_returned</i>
<span>{{ $t("import_from_gist") }}</span>
</button>
@@ -25,12 +42,16 @@
}"
>
<button
v-close-popover
:disabled="
!fb.currentUser ? true : fb.currentUser.provider !== 'github.com' ? true : false
!fb.currentUser
? true
: fb.currentUser.provider !== 'github.com'
? true
: false
"
class="icon"
@click="createCollectionGist"
v-close-popover
>
<i class="material-icons">assignment_turned_in</i>
<span>{{ $t("create_secret_gist") }}</span>
@@ -45,67 +66,127 @@
</div>
</div>
<div slot="body" class="flex flex-col">
<div class="flex flex-col items-start p-2">
<div v-if="mode == 'import_export'" class="flex flex-col items-start p-2">
<span
v-tooltip="{
content: !fb.currentUser ? $t('login_first') : $t('replace_current'),
content: !fb.currentUser
? $t('login_first')
: $t('replace_current'),
}"
>
<button :disabled="!fb.currentUser" class="icon" @click="syncCollections">
<button
v-if="collectionsType.type == 'my-collections'"
:disabled="!fb.currentUser"
class="icon"
@click="syncCollections"
>
<i class="material-icons">folder_shared</i>
<span>{{ $t("import_from_sync") }}</span>
</button>
</span>
<button
v-tooltip="$t('replace_current')"
class="icon"
@click="openDialogChooseFileToReplaceWith"
v-tooltip="$t('replace_current')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("replace_json") }}</span>
<input
type="file"
@change="replaceWithJSON"
style="display: none"
ref="inputChooseFileToReplaceWith"
type="file"
style="display: none"
accept="application/json"
@change="replaceWithJSON"
/>
</button>
<button
v-tooltip="$t('preserve_current')"
class="icon"
@click="openDialogChooseFileToImportFrom"
v-tooltip="$t('preserve_current')"
>
<i class="material-icons">folder_special</i>
<span>{{ $t("import_json") }}</span>
<input
type="file"
@change="importFromJSON"
style="display: none"
ref="inputChooseFileToImportFrom"
type="file"
style="display: none"
accept="application/json"
@change="importFromJSON"
/>
</button>
<button
v-if="collectionsType.type == 'team-collections'"
v-tooltip="$t('replace_current')"
class="icon"
@click="mode = 'import_from_my_collections'"
>
<i class="material-icons">folder_special</i>
<span>{{ "Import from My Collections" }}</span>
</button>
<button
v-tooltip="$t('show_code')"
class="icon"
@click="
() => {
mode = 'export_as_json'
getJSONCollection()
}
"
>
<i class="material-icons">folder_special</i>
<span>{{ "Export As JSON" }}</span>
</button>
</div>
<div v-if="showJsonCode" class="row-wrapper">
<div v-if="mode == 'import_from_my_collections'">
<span class="select-wrapper">
<select
type="text"
autofocus
@change="
($event) => {
mySelectedCollectionID = $event.target.value
}
"
>
<option
:key="undefined"
:value="undefined"
hidden
disabled
selected
>
Select Collection
</option>
<option
v-for="(collection, index) in myCollections"
:key="index"
:value="index"
>
{{ collection.name }}
</option>
</select>
</span>
<button
class="m-2 icon primary"
:disabled="mySelectedCollectionID == undefined"
@click="importFromMyCollections"
>
{{ $t("import") }}
</button>
</div>
<div v-if="mode == 'export_as_json'">
<textarea v-model="collectionJson" rows="8" readonly></textarea>
</div>
</div>
<div slot="footer">
<div class="row-wrapper">
<span>
<SmartToggle :on="showJsonCode" @change="showJsonCode = $event">
{{ $t("show_code") }}
</SmartToggle>
</span>
<span>
<button class="icon" @click="hideModal">
{{ $t("cancel") }}
</button>
<button class="icon primary" @click="exportJSON" v-tooltip="$t('download_file')">
{{ $t("export") }}
</button>
</span>
<div class="row-wrapper">
<span class="m-2">
<button
v-tooltip="$t('download_file')"
class="icon primary"
@click="exportJSON"
>
{{ $t("export") }}
</button>
</span>
</div>
</div>
</div>
</SmartModal>
@@ -114,23 +195,32 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
import * as teamUtils from "~/helpers/teams/utils"
export default {
props: {
show: Boolean,
collectionsType: { type: Object, default: () => {} },
},
data() {
return {
fb,
showJsonCode: false,
mode: "import_export",
mySelectedCollectionID: undefined,
collectionJson: "",
}
},
subscriptions() {
SYNC_COLLECTIONS: getSettingSubject("syncCollections")
},
props: {
show: Boolean,
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
computed: {
collectionJson() {
return JSON.stringify(this.$store.state.postwoman.collections, null, 2)
myCollections() {
return fb.currentUser !== null
? fb.currentCollections
: this.$store.state.postwoman.collections
},
},
methods: {
@@ -141,7 +231,7 @@ export default {
{
files: {
"hoppscotch-collections.json": {
content: this.collectionJson,
content: this.getJSONCollection(),
},
},
},
@@ -152,11 +242,11 @@ export default {
},
}
)
.then(({ html_url }) => {
.then((res) => {
this.$toast.success(this.$t("gist_created"), {
icon: "done",
})
window.open(html_url)
window.open(res.html_url)
})
.catch((error) => {
this.$toast.error(this.$t("something_went_wrong"), {
@@ -166,7 +256,7 @@ export default {
})
},
async readCollectionGist() {
let gist = prompt(this.$t("enter_gist_url"))
const gist = prompt(this.$t("enter_gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
@@ -175,8 +265,11 @@ export default {
},
})
.then(({ files }) => {
let collections = JSON.parse(Object.values(files)[0].content)
this.$store.commit("postwoman/replaceCollections", { data: collections, flag: "rest" })
const collections = JSON.parse(Object.values(files)[0].content)
this.$store.commit("postwoman/replaceCollections", {
data: collections,
flag: "rest",
})
this.fileImported()
this.syncToFBCollections()
})
@@ -186,6 +279,8 @@ export default {
})
},
hideModal() {
this.mode = "import_export"
this.mySelectedCollectionID = undefined
this.$emit("hide-modal")
},
openDialogChooseFileToReplaceWith() {
@@ -195,60 +290,157 @@ export default {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
let [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
this.$store.commit("postwoman/replaceCollections", { data: collections, flag: "rest" })
this.fileImported()
this.syncToFBCollections()
if (this.collectionsType.type === "team-collections") {
teamUtils
.replaceWithJSON(
this.$apollo,
collections,
this.collectionsType.selectedTeam.id
)
.then((status) => {
if (status) {
this.fileImported()
} else {
this.failedImport()
}
})
.catch((error) => {
console.log(error)
this.failedImport()
})
} else {
this.$store.commit("postwoman/replaceCollections", {
data: collections,
flag: "rest",
})
this.fileImported()
this.syncToFBCollections()
}
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
let [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
//replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>"))
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(
content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
)
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
this.$store.commit("postwoman/importCollections", { data: collections, flag: "rest" })
this.fileImported()
this.syncToFBCollections()
if (this.collectionsType.type === "team-collections") {
teamUtils
.importFromJSON(
this.$apollo,
collections,
this.collectionsType.selectedTeam.id
)
.then((status) => {
if (status) {
this.$emit("update-team-collections")
this.fileImported()
} else {
this.failedImport()
}
})
.catch((error) => {
console.log(error)
this.failedImport()
})
} else {
this.$store.commit("postwoman/importCollections", {
data: collections,
flag: "rest",
})
this.syncToFBCollections()
this.fileImported()
}
}
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = ""
},
importFromMyCollections() {
teamUtils
.importFromMyCollections(
this.$apollo,
this.mySelectedCollectionID,
this.collectionsType.selectedTeam.id
)
.then((success) => {
if (success) {
this.fileImported()
this.$emit("update-team-collections")
} else {
this.failedImport()
}
})
.catch((error) => {
console.log(error)
this.failedImport()
})
},
async getJSONCollection() {
if (this.collectionsType.type === "my-collections") {
this.collectionJson = JSON.stringify(
this.$store.state.postwoman.collections,
null,
2
)
} else {
this.collectionJson = await teamUtils.exportAsJSON(
this.$apollo,
this.collectionsType.selectedTeam.id
)
}
return this.collectionJson
},
exportJSON() {
let text = this.collectionJson
text = text.replace(/\n/g, "\r\n")
let blob = new Blob([text], {
const blob = new Blob([text], {
type: "text/json",
})
let anchor = document.createElement("a")
const anchor = document.createElement("a")
anchor.download = "hoppscotch-collection.json"
anchor.href = window.URL.createObjectURL(blob)
anchor.target = "_blank"
@@ -286,7 +478,7 @@ export default {
})
},
parsePostmanCollection({ info, name, item }) {
let postwomanCollection = {
const postwomanCollection = {
name: "",
folders: [],
requests: [],
@@ -295,26 +487,39 @@ export default {
postwomanCollection.name = info ? info.name : name
if (item && item.length > 0) {
for (let collectionItem of item) {
for (const collectionItem of item) {
if (collectionItem.request) {
if (postwomanCollection.hasOwnProperty("folders")) {
if (
Object.prototype.hasOwnProperty.call(
postwomanCollection,
"folders"
)
) {
postwomanCollection.name = info ? info.name : name
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
} else {
postwomanCollection.name = name ? name : ""
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.name = name || ""
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
} else if (this.hasFolder(collectionItem)) {
postwomanCollection.folders.push(this.parsePostmanCollection(collectionItem))
postwomanCollection.folders.push(
this.parsePostmanCollection(collectionItem)
)
} else {
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
}
}
return postwomanCollection
},
parsePostmanRequest({ name, request }) {
let pwRequest = {
const pwRequest = {
url: "",
path: "",
method: "",
@@ -334,20 +539,28 @@ export default {
}
pwRequest.name = name
let requestObjectUrl = request.url.raw.match(/^(.+:\/\/[^\/]+|{[^\/]+})(\/[^\?]+|).*$/)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
if (request.url) {
const requestObjectUrl = request.url.raw.match(
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
}
}
pwRequest.method = request.method
let itemAuth = request.auth ? request.auth : ""
let authType = itemAuth ? itemAuth.type : ""
const itemAuth = request.auth ? request.auth : ""
const authType = itemAuth ? itemAuth.type : ""
if (authType === "basic") {
pwRequest.auth = "Basic Auth"
pwRequest.httpUser =
itemAuth.basic[0].key === "username" ? itemAuth.basic[0].value : itemAuth.basic[1].value
itemAuth.basic[0].key === "username"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
pwRequest.httpPassword =
itemAuth.basic[0].key === "password" ? itemAuth.basic[0].value : itemAuth.basic[1].value
itemAuth.basic[0].key === "password"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
} else if (authType === "oauth2") {
pwRequest.auth = "OAuth 2.0"
pwRequest.bearerToken =
@@ -358,26 +571,28 @@ export default {
pwRequest.auth = "Bearer Token"
pwRequest.bearerToken = itemAuth.bearer[0].value
}
let requestObjectHeaders = request.header
const requestObjectHeaders = request.header
if (requestObjectHeaders) {
pwRequest.headers = requestObjectHeaders
for (let header of pwRequest.headers) {
for (const header of pwRequest.headers) {
delete header.name
delete header.type
}
}
let requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (let param of pwRequest.params) {
delete param.disabled
if (request.url) {
const requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (const param of pwRequest.params) {
delete param.disabled
}
}
}
if (request.body) {
if (request.body.mode === "urlencoded") {
let params = request.body.urlencoded
pwRequest.bodyParams = params ? params : []
for (let param of pwRequest.bodyParams) {
const params = request.body.urlencoded
pwRequest.bodyParams = params || []
for (const param of pwRequest.bodyParams) {
delete param.type
}
} else if (request.body.mode === "raw") {
@@ -388,7 +603,7 @@ export default {
return pwRequest
},
hasFolder(item) {
return item.hasOwnProperty("item")
return Object.prototype.hasOwnProperty.call(item, "item")
},
},
}

View File

@@ -12,46 +12,21 @@
</div>
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("token_req_name") }}</label>
<input type="text" id="selectLabel" v-model="requestData.name" @keyup.enter="saveRequestAs" />
<ul>
<li>
<label for="selectCollection">{{ $t("collection") }}</label>
<span class="select-wrapper">
<select type="text" id="selectCollection" v-model="requestData.collectionIndex">
<option :key="undefined" :value="undefined" hidden disabled selected>
{{ $t("select_collection") }}
</option>
<option
v-for="(collection, index) in $store.state.postwoman.collections"
:key="index"
:value="index"
>
{{ collection.name }}
</option>
</select>
</span>
</li>
</ul>
<label>{{ $t("folder") }}</label>
<SmartAutoComplete
:placeholder="$t('search')"
:source="folders"
:spellcheck="false"
v-model="requestData.folderName"
<input
id="selectLabel"
v-model="requestData.name"
type="text"
@keyup.enter="saveRequestAs"
/>
<label for="selectLabel">Select location</label>
<!-- <input readonly :value="path" /> -->
<Collections
:picked="picked"
:save-request="true"
@select="onSelect"
@update-collection="collectionsType.type = $event"
@update-coll-type="onUpdateCollType"
/>
<ul>
<li>
<label for="selectRequest">{{ $t("request") }}</label>
<span class="select-wrapper">
<select type="text" id="selectRequest" v-model="requestData.requestIndex">
<option :key="undefined" :value="undefined">/</option>
<option v-for="(folder, index) in requests" :key="index" :value="index">
{{ folder.name }}
</option>
</select>
</span>
</li>
</ul>
</div>
<div slot="footer">
<div class="row-wrapper">
@@ -72,21 +47,28 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
import * as teamUtils from "~/helpers/teams/utils"
export default {
props: {
show: Boolean,
editingRequest: Object,
editingRequest: { type: Object, default: () => {} },
},
data() {
return {
defaultRequestName: "Untitled Request",
path: "Path will appear here",
requestData: {
name: undefined,
collectionIndex: undefined,
folderName: undefined,
requestIndex: undefined,
},
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
picked: null,
}
},
subscriptions() {
@@ -94,20 +76,6 @@ export default {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
computed: {
folders() {
const collections = this.$store.state.postwoman.collections
@@ -130,7 +98,8 @@ export default {
return []
}
const userSelectedAnyFolder = folderName !== undefined && folderName !== ""
const userSelectedAnyFolder =
folderName !== undefined && folderName !== ""
if (userSelectedAnyFolder) {
const collection = collections[collectionIndex]
@@ -148,7 +117,27 @@ export default {
}
},
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
methods: {
onUpdateCollType(newCollType) {
this.collectionsType = newCollType
},
onSelect({ picked }) {
this.picked = picked
},
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
@@ -158,8 +147,7 @@ export default {
}
},
saveRequestAs() {
const userDidntSpecifyCollection = this.$data.requestData.collectionIndex === undefined
if (userDidntSpecifyCollection) {
if (this.picked == null) {
this.$toast.error(this.$t("select_collection"), {
icon: "error",
})
@@ -178,16 +166,61 @@ export default {
collection: this.$data.requestData.collectionIndex,
}
this.$store.commit("postwoman/saveRequestAs", {
request: requestUpdated,
collectionIndex: this.$data.requestData.collectionIndex,
folderName: this.$data.requestData.folderName,
requestIndex: this.$data.requestData.requestIndex,
flag: "rest",
})
if (this.picked.pickedType === "my-request") {
this.$store.commit("postwoman/saveRequestAs", {
request: requestUpdated,
collectionIndex: this.picked.collectionIndex,
folderName: this.picked.folderName,
requestIndex: this.picked.requestIndex,
flag: "rest",
})
this.syncCollections()
} else if (this.picked.pickedType === "my-folder") {
this.$store.commit("postwoman/saveRequestAs", {
request: requestUpdated,
collectionIndex: this.picked.collectionIndex,
folderName: this.picked.folderName,
flag: "rest",
})
this.syncCollections()
} else if (this.picked.pickedType === "my-collection") {
this.$store.commit("postwoman/saveRequestAs", {
request: requestUpdated,
collectionIndex: this.picked.collectionIndex,
flag: "rest",
})
this.syncCollections()
} else if (this.picked.pickedType === "teams-request") {
teamUtils.overwriteRequestTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.picked.requestID
)
} else if (this.picked.pickedType === "teams-folder") {
teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.folderID
)
} else if (this.picked.pickedType === "teams-collection") {
teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.collectionID
)
}
this.$toast.success("Requested added", {
icon: "done",
})
this.hideModal()
this.syncCollections()
},
hideModal() {
this.$emit("hide-modal")
@@ -195,12 +228,12 @@ export default {
},
}
function getFolderNames(folders, namesList) {
function getFolderNames(folders, namesList, folderName = "") {
if (folders.length) {
folders.forEach((folder) => {
namesList.push(folder.name)
namesList.push(folderName + folder.name)
if (folder.folders && folder.folders.length) {
getFolderNames(folder.folders, namesList)
getFolderNames(folder.folders, namesList, folder.name + "/")
}
})
}

View File

@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="$t('my_new_collection')"
@keyup.enter="addNewCollection"
/>
@@ -45,7 +45,7 @@ export default {
},
data() {
return {
name: undefined,
name: null,
}
},
methods: {
@@ -53,7 +53,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
@@ -68,12 +70,12 @@ export default {
name: this.$data.name,
flag: "graphql",
})
this.$emit("hide-modal")
this.syncCollections()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
this.$data.name = undefined
},
},
}

View File

@@ -1,5 +1,5 @@
<template>
<SmartModal v-if="show" @close="show = false">
<SmartModal v-if="show" @close="$emit('hide-modal')">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("new_folder") }}</h3>
@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="$t('my_new_folder')"
@keyup.enter="addFolder"
/>
@@ -37,27 +37,44 @@
</template>
<script>
import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
folder: Object,
folderPath: String,
collectionIndex: Number,
folder: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
},
data() {
return {
name: undefined,
name: null,
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
}
},
addFolder() {
this.$emit("add-folder", {
name: this.name,
folder: this.folder,
path: this.folderPath || `${this.collectionIndex}`,
})
this.syncCollections()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@@ -10,43 +13,60 @@
@dragend="dragging = false"
>
<button class="icon" @click="toggleShowChildren">
<i class="material-icons" v-show="!showChildren && !isFiltered">arrow_right</i>
<i class="material-icons" v-show="showChildren || isFiltered">arrow_drop_down</i>
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i class="material-icons">folder</i>
<span>{{ collection.name }}</span>
</button>
<div>
<button
v-if="doc"
v-tooltip.left="$t('import')"
class="icon"
@click="$emit('select-collection')"
v-tooltip.left="$t('import')"
>
<i class="material-icons">topic</i>
</button>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
class="icon"
@click="$emit('add-folder', { folder: collection, path: `${collectionIndex}` })"
v-close-popover
class="icon"
@click="
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
</button>
</div>
<div>
<button class="icon" @click="$emit('edit-collection')" v-close-popover>
<button
v-close-popover
class="icon"
@click="$emit('edit-collection')"
>
<i class="material-icons">create</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button
v-close-popover
class="icon"
@click="confirmRemove = true"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -68,7 +88,7 @@
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:doc="doc"
:isFiltered="isFiltered"
:is-filtered="isFiltered"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@@ -94,11 +114,14 @@
</ul>
<ul>
<li
v-if="collection.folders.length === 0 && collection.requests.length === 0"
v-if="
collection.folders.length === 0 && collection.requests.length === 0
"
class="flex ml-8 border-l border-brdColor"
>
<p class="info">
<i class="material-icons">not_interested</i> {{ $t("collection_empty") }}
<i class="material-icons">not_interested</i>
{{ $t("collection_empty") }}
</p>
</li>
</ul>
@@ -117,8 +140,8 @@ import { fb } from "~/helpers/fb"
export default {
props: {
collectionIndex: Number,
collection: Object,
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => {} },
doc: Boolean,
isFiltered: Boolean,
},
@@ -135,7 +158,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}

View File

@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="editingCollection.name"
@keyup.enter="saveCollection"
/>
@@ -42,12 +42,12 @@ import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
editingCollection: Object,
editingCollectionIndex: Number,
editingCollection: { type: Object, default: () => {} },
editingCollectionIndex: { type: Number, default: null },
},
data() {
return {
name: undefined,
name: null,
}
},
methods: {
@@ -55,7 +55,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
@@ -75,10 +77,11 @@ export default {
collectionIndex: this.$props.editingCollectionIndex,
flag: "graphql",
})
this.$emit("hide-modal")
this.syncCollections()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -1,5 +1,5 @@
<template>
<SmartModal v-if="show" @close="show = false">
<SmartModal v-if="show" @close="$emit('hide-modal')">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("edit_folder") }}</h3>
@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="folder.name"
@keyup.enter="editFolder"
/>
@@ -42,13 +42,13 @@ import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
collectionIndex: Number,
folder: Object,
folderIndex: Number,
collectionIndex: { type: Number, default: null },
folder: { type: Object, default: () => {} },
folderIndex: { type: Number, default: null },
},
data() {
return {
name: undefined,
name: null,
}
},
methods: {
@@ -56,7 +56,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
@@ -70,10 +72,11 @@ export default {
folderName: this.$props.folder.name,
flag: "graphql",
})
this.hideModal()
this.syncCollections()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},

View File

@@ -13,11 +13,11 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="requestUpdateData.name"
@keyup.enter="saveRequest"
type="text"
:placeholder="request.name"
@keyup.enter="saveRequest"
/>
</div>
<div slot="footer">
@@ -42,16 +42,16 @@ import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
collectionIndex: Number,
folderIndex: Number,
folderName: String,
request: Object,
requestIndex: Number,
collectionIndex: { type: Number, default: null },
folderIndex: { type: Number, default: null },
folderName: { type: String, default: null },
request: { type: Object, default: () => {} },
requestIndex: { type: Number, default: null },
},
data() {
return {
requestUpdateData: {
name: undefined,
name: null,
},
}
},
@@ -60,7 +60,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
@@ -80,11 +82,11 @@ export default {
requestIndex: this.$props.requestIndex,
flag: "graphql",
})
this.hideModal()
this.syncCollections()
this.hideModal()
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@@ -11,22 +14,26 @@
>
<div>
<button class="icon" @click="toggleShowChildren">
<i class="material-icons" v-show="!showChildren && !isFiltered">arrow_right</i>
<i class="material-icons" v-show="showChildren || isFiltered">arrow_drop_down</i>
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i class="material-icons">folder_open</i>
<span>{{ folder.name }}</span>
</button>
</div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
class="icon"
@click="$emit('add-folder', { folder, path: folderPath })"
v-close-popover
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
@@ -34,16 +41,18 @@
</div>
<div>
<button
class="icon"
@click="$emit('edit-folder', { folder, folderIndex, collectionIndex })"
v-close-popover
class="icon"
@click="
$emit('edit-folder', { folder, folderIndex, collectionIndex })
"
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -52,7 +61,7 @@
</v-popover>
</div>
<div v-show="showChildren || isFiltered">
<ul v-if="folder.folders && folder.folders.length" class="flex-col">
<ul class="flex-col">
<li
v-for="(subFolder, subFolderIndex) in folder.folders"
:key="subFolder.name"
@@ -61,9 +70,10 @@
<CollectionsGraphqlFolder
:folder="subFolder"
:folder-index="subFolderIndex"
:folder-path="`${folderPath}/${subFolderIndex}`"
:collection-index="collectionIndex"
:doc="doc"
:folder-path="`${folderPath}/${subFolderIndex}`"
:is-filtered="isFiltered"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@@ -96,7 +106,10 @@
"
>
<li class="flex ml-8 border-l border-brdColor">
<p class="info"><i class="material-icons">not_interested</i> {{ $t("folder_empty") }}</p>
<p class="info">
<i class="material-icons">not_interested</i>
{{ $t("folder_empty") }}
</p>
</li>
</ul>
</div>
@@ -113,12 +126,12 @@
import { fb } from "~/helpers/fb"
export default {
name: "folder",
name: "Folder",
props: {
folder: Object,
folderIndex: Number,
collectionIndex: Number,
folderPath: String,
folder: { type: Object, default: () => {} },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
doc: Boolean,
isFiltered: Boolean,
},
@@ -134,7 +147,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}

View File

@@ -5,12 +5,16 @@
<h3 class="title">{{ $t("import_export") }} {{ $t("collections") }}</h3>
<div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="readCollectionGist" v-close-popover>
<button
v-close-popover
class="icon"
@click="readCollectionGist"
>
<i class="material-icons">assignment_returned</i>
<span>{{ $t("import_from_gist") }}</span>
</button>
@@ -25,12 +29,16 @@
}"
>
<button
v-close-popover
:disabled="
!fb.currentUser ? true : fb.currentUser.provider !== 'github.com' ? true : false
!fb.currentUser
? true
: fb.currentUser.provider !== 'github.com'
? true
: false
"
class="icon"
@click="createCollectionGist"
v-close-popover
>
<i class="material-icons">assignment_turned_in</i>
<span>{{ $t("create_secret_gist") }}</span>
@@ -48,42 +56,48 @@
<div class="flex flex-col items-start p-2">
<span
v-tooltip="{
content: !fb.currentUser ? $t('login_first') : $t('replace_current'),
content: !fb.currentUser
? $t('login_first')
: $t('replace_current'),
}"
>
<button :disabled="!fb.currentUser" class="icon" @click="syncCollections">
<button
:disabled="!fb.currentUser"
class="icon"
@click="syncCollections"
>
<i class="material-icons">folder_shared</i>
<span>{{ $t("import_from_sync") }}</span>
</button>
</span>
<button
v-tooltip="$t('replace_current')"
class="icon"
@click="openDialogChooseFileToReplaceWith"
v-tooltip="$t('replace_current')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("replace_json") }}</span>
<input
type="file"
@change="replaceWithJSON"
style="display: none"
ref="inputChooseFileToReplaceWith"
type="file"
style="display: none"
accept="application/json"
@change="replaceWithJSON"
/>
</button>
<button
v-tooltip="$t('preserve_current')"
class="icon"
@click="openDialogChooseFileToImportFrom"
v-tooltip="$t('preserve_current')"
>
<i class="material-icons">folder_special</i>
<span>{{ $t("import_json") }}</span>
<input
type="file"
@change="importFromJSON"
style="display: none"
ref="inputChooseFileToImportFrom"
type="file"
style="display: none"
accept="application/json"
@change="importFromJSON"
/>
</button>
</div>
@@ -102,7 +116,11 @@
<button class="icon" @click="hideModal">
{{ $t("cancel") }}
</button>
<button class="icon primary" @click="exportJSON" v-tooltip="$t('download_file')">
<button
v-tooltip="$t('download_file')"
class="icon primary"
@click="exportJSON"
>
{{ $t("export") }}
</button>
</span>
@@ -115,18 +133,22 @@
import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
},
data() {
return {
fb,
showJsonCode: false,
}
},
props: {
show: Boolean,
},
computed: {
collectionJson() {
return JSON.stringify(this.$store.state.postwoman.collectionsGraphql, null, 2)
return JSON.stringify(
this.$store.state.postwoman.collectionsGraphql,
null,
2
)
},
},
methods: {
@@ -148,11 +170,11 @@ export default {
},
}
)
.then(({ html_url }) => {
.then((res) => {
this.$toast.success(this.$t("gist_created"), {
icon: "done",
})
window.open(html_url)
window.open(res.html_url)
})
.catch((error) => {
this.$toast.error(this.$t("something_went_wrong"), {
@@ -162,7 +184,7 @@ export default {
})
},
async readCollectionGist() {
let gist = prompt(this.$t("enter_gist_url"))
const gist = prompt(this.$t("enter_gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
@@ -171,8 +193,11 @@ export default {
},
})
.then(({ files }) => {
let collections = JSON.parse(Object.values(files)[0].content)
this.$store.commit("postwoman/replaceCollections", { data: collections, flag: "graphql" })
const collections = JSON.parse(Object.values(files)[0].content)
this.$store.commit("postwoman/replaceCollections", {
data: collections,
flag: "graphql",
})
this.fileImported()
this.syncToFBCollections()
})
@@ -191,22 +216,32 @@ export default {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
let [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
this.$store.commit("postwoman/replaceCollections", { data: collections, flag: "graphql" })
this.$store.commit("postwoman/replaceCollections", {
data: collections,
flag: "graphql",
})
this.fileImported()
this.syncToFBCollections()
}
@@ -214,24 +249,36 @@ export default {
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
let [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
//replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>"))
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(
content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
)
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
this.$store.commit("postwoman/importCollections", { data: collections, flag: "graphql" })
this.$store.commit("postwoman/importCollections", {
data: collections,
flag: "graphql",
})
this.fileImported()
this.syncToFBCollections()
}
@@ -241,10 +288,10 @@ export default {
exportJSON() {
let text = this.collectionJson
text = text.replace(/\n/g, "\r\n")
let blob = new Blob([text], {
const blob = new Blob([text], {
type: "text/json",
})
let anchor = document.createElement("a")
const anchor = document.createElement("a")
anchor.download = "hoppscotch-collection.json"
anchor.href = window.URL.createObjectURL(blob)
anchor.target = "_blank"
@@ -267,7 +314,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
@@ -284,7 +333,7 @@ export default {
})
},
parsePostmanCollection({ info, name, item }) {
let postwomanCollection = {
const postwomanCollection = {
name: "",
folders: [],
requests: [],
@@ -293,26 +342,39 @@ export default {
postwomanCollection.name = info ? info.name : name
if (item && item.length > 0) {
for (let collectionItem of item) {
for (const collectionItem of item) {
if (collectionItem.request) {
if (postwomanCollection.hasOwnProperty("folders")) {
if (
Object.prototype.hasOwnProperty.call(
postwomanCollection,
"folders"
)
) {
postwomanCollection.name = info ? info.name : name
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
} else {
postwomanCollection.name = name ? name : ""
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.name = name || ""
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
} else if (this.hasFolder(collectionItem)) {
postwomanCollection.folders.push(this.parsePostmanCollection(collectionItem))
postwomanCollection.folders.push(
this.parsePostmanCollection(collectionItem)
)
} else {
postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem))
postwomanCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
}
}
return postwomanCollection
},
parsePostmanRequest({ name, request }) {
let pwRequest = {
const pwRequest = {
url: "",
path: "",
method: "",
@@ -332,20 +394,26 @@ export default {
}
pwRequest.name = name
let requestObjectUrl = request.url.raw.match(/^(.+:\/\/[^\/]+|{[^\/]+})(\/[^\?]+|).*$/)
const requestObjectUrl = request.url.raw.match(
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
}
pwRequest.method = request.method
let itemAuth = request.auth ? request.auth : ""
let authType = itemAuth ? itemAuth.type : ""
const itemAuth = request.auth ? request.auth : ""
const authType = itemAuth ? itemAuth.type : ""
if (authType === "basic") {
pwRequest.auth = "Basic Auth"
pwRequest.httpUser =
itemAuth.basic[0].key === "username" ? itemAuth.basic[0].value : itemAuth.basic[1].value
itemAuth.basic[0].key === "username"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
pwRequest.httpPassword =
itemAuth.basic[0].key === "password" ? itemAuth.basic[0].value : itemAuth.basic[1].value
itemAuth.basic[0].key === "password"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
} else if (authType === "oauth2") {
pwRequest.auth = "OAuth 2.0"
pwRequest.bearerToken =
@@ -356,26 +424,26 @@ export default {
pwRequest.auth = "Bearer Token"
pwRequest.bearerToken = itemAuth.bearer[0].value
}
let requestObjectHeaders = request.header
const requestObjectHeaders = request.header
if (requestObjectHeaders) {
pwRequest.headers = requestObjectHeaders
for (let header of pwRequest.headers) {
for (const header of pwRequest.headers) {
delete header.name
delete header.type
}
}
let requestObjectParams = request.url.query
const requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (let param of pwRequest.params) {
for (const param of pwRequest.params) {
delete param.disabled
}
}
if (request.body) {
if (request.body.mode === "urlencoded") {
let params = request.body.urlencoded
pwRequest.bodyParams = params ? params : []
for (let param of pwRequest.bodyParams) {
const params = request.body.urlencoded
pwRequest.bodyParams = params || []
for (const param of pwRequest.bodyParams) {
delete param.type
}
} else if (request.body.mode === "raw") {
@@ -386,7 +454,7 @@ export default {
return pwRequest
},
hasFolder(item) {
return item.hasOwnProperty("item")
return Object.prototype.hasOwnProperty.call(item, "item")
},
},
}

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@@ -10,20 +13,22 @@
>
<div>
<button
v-tooltip="!doc ? $t('use_request') : ''"
class="icon"
@click="!doc ? selectRequest() : {}"
v-tooltip="!doc ? $t('use_request') : ''"
>
<i class="material-icons">description</i>
<span>{{ request.name }}</span>
</button>
</div>
<v-popover>
<button class="tooltip-target icon" v-tooltip="$t('more')">
<button v-tooltip="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
class="icon"
@click="
$emit('edit-request', {
@@ -34,14 +39,13 @@
requestIndex,
})
"
v-close-popover
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -63,11 +67,11 @@ import { fb } from "~/helpers/fb"
export default {
props: {
request: Object,
collectionIndex: Number,
folderIndex: Number,
folderName: String,
requestIndex: Number,
request: { type: Object, default: () => {} },
collectionIndex: { type: Number, default: null },
folderIndex: { type: Number, default: null },
folderName: { type: String, default: null },
requestIndex: { type: Number, default: null },
doc: Boolean,
},
data() {
@@ -81,14 +85,18 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
}
},
selectRequest() {
this.$store.commit("postwoman/selectGraphqlRequest", { request: this.request })
this.$store.commit("postwoman/selectGraphqlRequest", {
request: this.request,
})
},
dragStart({ dataTransfer }) {
this.dragging = !this.dragging

View File

@@ -12,17 +12,33 @@
</div>
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("token_req_name") }}</label>
<input type="text" id="selectLabel" v-model="requestData.name" @keyup.enter="saveRequestAs" />
<input
id="selectLabel"
v-model="requestData.name"
type="text"
@keyup.enter="saveRequestAs"
/>
<ul>
<li>
<label for="selectCollection">{{ $t("collection") }}</label>
<span class="select-wrapper">
<select type="text" id="selectCollection" v-model="requestData.collectionIndex">
<option :key="undefined" :value="undefined" hidden disabled selected>
<select
id="selectCollection"
v-model="requestData.collectionIndex"
type="text"
>
<option
:key="undefined"
:value="undefined"
hidden
disabled
selected
>
{{ $t("select_collection") }}
</option>
<option
v-for="(collection, index) in $store.state.postwoman.collectionsGraphql"
v-for="(collection, index) in $store.state.postwoman
.collectionsGraphql"
:key="index"
:value="index"
>
@@ -34,18 +50,26 @@
</ul>
<label>{{ $t("folder") }}</label>
<SmartAutoComplete
v-model="requestData.folderName"
:placeholder="$t('search')"
:source="folders"
:spellcheck="false"
v-model="requestData.folderName"
/>
<ul>
<li>
<label for="selectRequest">{{ $t("request") }}</label>
<span class="select-wrapper">
<select type="text" id="selectRequest" v-model="requestData.requestIndex">
<select
id="selectRequest"
v-model="requestData.requestIndex"
type="text"
>
<option :key="undefined" :value="undefined">/</option>
<option v-for="(folder, index) in requests" :key="index" :value="index">
<option
v-for="(folder, index) in requests"
:key="index"
:value="index"
>
{{ folder.name }}
</option>
</select>
@@ -75,7 +99,7 @@ import { fb } from "~/helpers/fb"
export default {
props: {
show: Boolean,
editingRequest: Object,
editingRequest: { type: Object, default: () => {} },
},
data() {
return {
@@ -88,20 +112,6 @@ export default {
},
}
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
computed: {
folders() {
const collections = this.$store.state.postwoman.collectionsGraphql
@@ -124,7 +134,8 @@ export default {
return []
}
const userSelectedAnyFolder = folderName !== undefined && folderName !== ""
const userSelectedAnyFolder =
folderName !== undefined && folderName !== ""
if (userSelectedAnyFolder) {
const collection = collections[collectionIndex]
@@ -142,19 +153,36 @@ export default {
}
},
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
}
},
saveRequestAs() {
const userDidntSpecifyCollection = this.$data.requestData.collectionIndex === undefined
const userDidntSpecifyCollection =
this.$data.requestData.collectionIndex === undefined
if (userDidntSpecifyCollection) {
this.$toast.error(this.$t("select_collection"), {
icon: "error",

View File

@@ -1,15 +1,23 @@
<template>
<AppSection class="yellow" :label="$t('collections')" ref="collections" no-legend>
<AppSection
ref="collections"
class="yellow"
:label="$t('collections')"
no-legend
>
<div class="show-on-large-screen">
<input
v-model="filterText"
aria-label="Search"
type="search"
:placeholder="$t('search')"
v-model="filterText"
class="rounded-t-lg"
/>
</div>
<CollectionsGraphqlAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
<CollectionsGraphqlAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
/>
<CollectionsGraphqlEdit
:show="showModalEdit"
:editing-collection="editingCollection"
@@ -53,17 +61,21 @@
</button>
</div>
<p v-if="collections.length === 0" class="info">
<i class="material-icons">help_outline</i> {{ $t("create_new_collection") }}
<i class="material-icons">help_outline</i>
{{ $t("create_new_collection") }}
</p>
<div class="virtual-list">
<ul class="flex-col">
<li v-for="(collection, index) in filteredCollections" :key="collection.name">
<li
v-for="(collection, index) in filteredCollections"
:key="collection.name"
>
<CollectionsGraphqlCollection
:name="collection.name"
:collection-index="index"
:collection="collection"
:doc="doc"
:isFiltered="filterText.length > 0"
:is-filtered="filterText.length > 0"
@edit-collection="editCollection(collection, index)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@@ -74,17 +86,13 @@
</ul>
</div>
<p v-if="filterText && filteredCollections.length === 0" class="info">
<i class="material-icons">not_interested</i> {{ $t("nothing_found") }} "{{ filterText }}"
<i class="material-icons">not_interested</i> {{ $t("nothing_found") }} "{{
filterText
}}"
</p>
</AppSection>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>
<script>
import { fb } from "~/helpers/fb"
@@ -128,15 +136,16 @@ export default {
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
for (let collection of collections) {
for (const collection of collections) {
const filteredRequests = []
const filteredFolders = []
for (let request of collection.requests) {
if (request.name.toLowerCase().includes(filterText)) filteredRequests.push(request)
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (let folder of collection.folders) {
for (const folder of collection.folders) {
const filteredFolderRequests = []
for (let request of folder.requests) {
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
@@ -158,15 +167,24 @@ export default {
return filteredCollections
},
},
async mounted() {
mounted() {
this._keyListener = function (e) {
if (e.key === "Escape") {
e.preventDefault()
this.showModalAdd = this.showModalEdit = this.showModalImportExport = this.showModalAddFolder = this.showModalEditFolder = this.showModalEditRequest = false
this.showModalAdd =
this.showModalEdit =
this.showModalImportExport =
this.showModalAddFolder =
this.showModalEditFolder =
this.showModalEditRequest =
false
}
}
document.addEventListener("keydown", this._keyListener.bind(this))
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
@@ -226,7 +244,13 @@ export default {
this.syncCollections()
},
editRequest(payload) {
const { collectionIndex, folderIndex, folderName, request, requestIndex } = payload
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
} = payload
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
@@ -247,15 +271,20 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)),
JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql"
)
}
}
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
}
</script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>

View File

@@ -1,20 +1,32 @@
<template>
<AppSection :label="$t('collections')" ref="collections" no-legend>
<AppSection ref="collections" :label="$t('collections')" no-legend>
<div class="show-on-large-screen">
<input
v-if="!saveRequest"
v-model="filterText"
aria-label="Search"
type="search"
:placeholder="$t('search')"
v-model="filterText"
class="rounded-t-lg"
/>
</div>
<CollectionsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
<CollectionsChooseType
:collections-type="collectionsType"
:show="showTeamCollections"
:doc="doc"
@update-collection-type="updateCollectionType"
@update-selected-team="updateSelectedTeam"
/>
<CollectionsAdd
:show="showModalAdd"
@submit="addNewRootCollection"
@hide-modal="displayModalAdd(false)"
/>
<CollectionsEdit
:show="showModalEdit"
:editing-collection="editingCollection"
:editing-collection-index="editingCollectionIndex"
:editing-coll-name="editingCollection ? editingCollection.name : ''"
@hide-modal="displayModalEdit(false)"
@submit="updateEditingCollection"
/>
<CollectionsAddFolder
:show="showModalAddFolder"
@@ -25,73 +37,120 @@
/>
<CollectionsEditFolder
:show="showModalEditFolder"
:collection-index="editingCollectionIndex"
:folder="editingFolder"
:folder-index="editingFolderIndex"
@submit="updateEditingFolder"
@hide-modal="displayModalEditFolder(false)"
/>
<CollectionsEditRequest
:show="showModalEditRequest"
:collection-index="editingCollectionIndex"
:folder-index="editingFolderIndex"
:folder-name="editingFolderName"
:request="editingRequest"
:request-index="editingRequestIndex"
:placeholder-req-name="editingRequest ? editingRequest.name : ''"
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsImportExport
:show="showModalImportExport"
:collections-type="collectionsType"
@hide-modal="displayModalImportExport(false)"
@update-team-collections="updateTeamCollections"
/>
<div class="border-b row-wrapper border-brdColor">
<button class="icon" @click="displayModalAdd(true)">
<button
v-if="
collectionsType.type == 'team-collections' &&
(collectionsType.selectedTeam == undefined ||
collectionsType.selectedTeam.myRole == 'VIEWER') &&
!saveRequest
"
class="icon"
disabled
@click="displayModalAdd(true)"
>
<i class="material-icons">add</i>
<div v-tooltip.left="$t('disable_new_collection')">
<span>{{ $t("new") }}</span>
</div>
</button>
<button
v-else-if="!saveRequest"
class="icon"
@click="displayModalAdd(true)"
>
<i class="material-icons">add</i>
<span>{{ $t("new") }}</span>
</button>
<button class="icon" @click="displayModalImportExport(true)">
<button
v-if="!saveRequest"
:disabled="
collectionsType.type == 'team-collections' &&
collectionsType.selectedTeam == undefined
"
class="icon"
@click="displayModalImportExport(true)"
>
{{ $t("import_export") }}
</button>
</div>
<p v-if="collections.length === 0" class="info">
<i class="material-icons">help_outline</i> {{ $t("create_new_collection") }}
<i class="material-icons">help_outline</i>
{{ $t("create_new_collection") }}
</p>
<div class="virtual-list">
<ul class="flex-col">
<li v-for="(collection, index) in filteredCollections" :key="collection.name">
<CollectionsCollection
<li
v-for="(collection, index) in filteredCollections"
:key="collection.name"
>
<component
:is="
collectionsType.type == 'my-collections'
? 'CollectionsMyCollection'
: 'CollectionsTeamsCollection'
"
:name="collection.name"
:collection-index="index"
:collection="collection"
:doc="doc"
:isFiltered="filterText.length > 0"
:is-filtered="filterText.length > 0"
:selected="selected.some((coll) => coll == collection)"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
@edit-collection="editCollection(collection, index)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@edit-request="editRequest($event)"
@update-team-collections="updateTeamCollections"
@select-collection="$emit('use-collection', collection)"
@unselect-collection="$emit('remove-collection', collection)"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-collection="removeCollection"
@remove-request="removeRequest"
/>
</li>
</ul>
</div>
<p v-if="filterText && filteredCollections.length === 0" class="info">
<i class="material-icons">not_interested</i> {{ $t("nothing_found") }} "{{ filterText }}"
<i class="material-icons">not_interested</i> {{ $t("nothing_found") }} "{{
filterText
}}"
</p>
</AppSection>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>
<script>
import gql from "graphql-tag"
import cloneDeep from "lodash/cloneDeep"
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import * as teamUtils from "~/helpers/teams/utils"
export default {
props: {
doc: Boolean,
selected: { type: Array, default: () => [] },
saveRequest: Boolean,
picked: { type: Object, default: () => {} },
},
data() {
return {
@@ -110,6 +169,12 @@ export default {
editingRequest: undefined,
editingRequestIndex: undefined,
filterText: "",
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
teamCollectionAdapter: new TeamCollectionAdapter(null),
teamCollectionsNew: [],
}
},
subscriptions() {
@@ -118,29 +183,51 @@ export default {
}
},
computed: {
showTeamCollections() {
if (fb.currentUser == null) {
return false
}
return true
},
collections() {
return fb.currentUser !== null
? fb.currentCollections
: this.$store.state.postwoman.collections
},
filteredCollections() {
const collections =
fb.currentUser !== null ? fb.currentCollections : this.$store.state.postwoman.collections
let collections = null
if (this.collectionsType.type === "my-collections") {
collections =
fb.currentUser !== null
? fb.currentCollections
: this.$store.state.postwoman.collections
} else {
collections = this.teamCollectionsNew
}
if (!this.filterText) return collections
if (!this.filterText) {
return collections
}
if (this.collectionsType.type === "team-collections") {
return []
}
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
for (let collection of collections) {
for (const collection of collections) {
const filteredRequests = []
const filteredFolders = []
for (let request of collection.requests) {
if (request.name.toLowerCase().includes(filterText)) filteredRequests.push(request)
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (let folder of collection.folders) {
for (const folder of this.collectionsType.type === "team-collections"
? collection.children
: collection.folders) {
const filteredFolderRequests = []
for (let request of folder.requests) {
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
@@ -151,7 +238,10 @@ export default {
}
}
if (filteredRequests.length + filteredFolders.length > 0) {
if (
filteredRequests.length + filteredFolders.length > 0 ||
collection.name.toLowerCase().includes(filterText)
) {
const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
@@ -162,16 +252,202 @@ export default {
return filteredCollections
},
},
async mounted() {
watch: {
"collectionsType.type": function emitstuff() {
this.$emit("update-collection", this.$data.collectionsType.type)
},
"collectionsType.selectedTeam"(value) {
if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
},
},
mounted() {
this._keyListener = function (e) {
if (e.key === "Escape") {
e.preventDefault()
this.showModalAdd = this.showModalEdit = this.showModalImportExport = this.showModalAddFolder = this.showModalEditFolder = this.showModalEditRequest = false
this.showModalAdd =
this.showModalEdit =
this.showModalImportExport =
this.showModalAddFolder =
this.showModalEditFolder =
this.showModalEditRequest =
false
}
}
document.addEventListener("keydown", this._keyListener.bind(this))
this.$subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
this.teamCollectionsNew = cloneDeep(colls)
})
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
methods: {
updateTeamCollections() {
// TODO: Remove this at some point
},
updateSelectedTeam(newSelectedTeam) {
this.collectionsType.selectedTeam = newSelectedTeam
this.$emit("update-coll-type", this.collectionsType)
},
updateCollectionType(newCollectionType) {
this.collectionsType.type = newCollectionType
this.$emit("update-coll-type", this.collectionsType)
},
// Intented to be called by the CollectionAdd modal submit event
addNewRootCollection(name) {
if (!name) {
this.$toast.info(this.$t("invalid_collection_name"))
return
}
if (this.collectionsType.type === "my-collections") {
this.$store.commit("postwoman/addNewCollection", {
name,
flag: "rest",
})
this.syncCollections()
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
teamUtils
.createNewRootCollection(
this.$apollo,
name,
this.collectionsType.selectedTeam.id
)
.then(() => {
this.$toast.success(this.$t("collection_added"), {
icon: "done",
})
})
.catch((error) => {
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
this.displayModalAdd(false)
},
// Intented to be called by CollectionEdit modal submit event
updateEditingCollection(newName) {
if (!newName) {
this.$toast.info(this.$t("invalid_collection_name"))
return
}
if (this.collectionsType.type === "my-collections") {
const collectionUpdated = {
...this.editingCollection,
name: newName,
}
this.$store.commit("postwoman/editCollection", {
collection: collectionUpdated,
collectionIndex: this.editingCollectionIndex,
flag: "rest",
})
this.syncCollections()
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
teamUtils
.renameCollection(this.$apollo, newName, this.editingCollection.id)
.then(() => {
// TODO: $t translations ?
this.$toast.success("Collection Renamed", {
icon: "done",
})
})
.catch((error) => {
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
this.displayModalEdit(false)
},
// Intended to be called by CollectionEditFolder modal submit event
updateEditingFolder(name) {
if (this.collectionsType.type === "my-collections") {
this.$store.commit("postwoman/editFolder", {
collectionIndex: this.editingCollectionIndex,
folder: { ...this.editingFolder, name },
folderIndex: this.editingFolderIndex,
folderName: this.editingFolder.name,
flag: "rest",
})
this.syncCollections()
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
teamUtils
.renameCollection(this.$apollo, name, this.editingFolder.id)
.then(() => {
// Result
this.$toast.success(this.$t("folder_renamed"), {
icon: "done",
})
})
.catch((error) => {
// Error
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
this.displayModalEditFolder(false)
},
// Intented to by called by CollectionsEditRequest modal submit event
updateEditingRequest(requestUpdateData) {
const requestUpdated = {
...this.editingRequest,
name: requestUpdateData.name || this.editingRequest.name,
}
if (this.collectionsType.type === "my-collections") {
this.$store.commit("postwoman/editRequest", {
requestCollectionIndex: this.editingCollectionIndex,
requestFolderName: this.editingFolderName,
requestFolderIndex: this.editingFolderIndex,
requestNew: requestUpdated,
requestIndex: this.editingRequestIndex,
flag: "rest",
})
this.syncCollections()
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
const requestName = requestUpdateData.name || this.editingRequest.name
teamUtils
.updateRequest(
this.$apollo,
requestUpdated,
requestName,
this.editingRequestIndex
)
.then(() => {
this.$toast.success("Request Renamed", {
icon: "done",
})
this.$emit("update-team-collections")
})
.catch((error) => {
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
this.displayModalEditRequest(false)
},
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
@@ -204,16 +480,56 @@ export default {
this.displayModalEdit(true)
this.syncCollections()
},
onAddFolder({ name, path }) {
onAddFolder({ name, folder, path }) {
const flag = "rest"
this.$store.commit("postwoman/addFolder", {
name,
path,
flag,
})
if (this.collectionsType.type === "my-collections") {
this.$store.commit("postwoman/addFolder", {
name,
path,
flag,
})
this.syncCollections()
} else if (this.collectionsType.type === "team-collections") {
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
this.$apollo
.mutate({
mutation: gql`
mutation CreateChildCollection(
$childTitle: String!
$collectionID: String!
) {
createChildCollection(
childTitle: $childTitle
collectionID: $collectionID
) {
id
}
}
`,
// Parameters
variables: {
childTitle: name,
collectionID: folder.id,
},
})
.then(() => {
// Result
this.$toast.success(this.$t("folder_added"), {
icon: "done",
})
this.$emit("update-team-collections")
})
.catch((error) => {
// Error
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
}
this.displayModalAddFolder(false)
this.syncCollections()
},
addFolder(payload) {
const { folder, path } = payload
@@ -226,16 +542,24 @@ export default {
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolder = folder
this.$data.editingFolderIndex = folderIndex
this.$data.collectionsType = this.collectionsType
this.displayModalEditFolder(true)
this.syncCollections()
},
editRequest(payload) {
const { collectionIndex, folderIndex, folderName, request, requestIndex } = payload
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
} = payload
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex
this.$emit("select-request", requestIndex)
this.displayModalEditRequest(true)
this.syncCollections()
},
@@ -255,9 +579,86 @@ export default {
)
}
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
expandCollection(collectionID) {
this.teamCollectionAdapter.expandCollection(collectionID)
},
removeCollection({ collectionsType, collectionIndex, collectionID }) {
if (collectionsType.type === "my-collections") {
this.$store.commit("postwoman/removeCollection", {
collectionIndex,
flag: "rest",
})
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
this.syncCollections()
} else if (collectionsType.type === "team-collections") {
if (collectionsType.selectedTeam.myRole !== "VIEWER") {
this.$apollo
.mutate({
// Query
mutation: gql`
mutation ($collectionID: String!) {
deleteCollection(collectionID: $collectionID)
}
`,
// Parameters
variables: {
collectionID,
},
})
.then(() => {
// Result
this.$toast.success(this.$t("deleted"), {
icon: "delete",
})
})
.catch((error) => {
// Error
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
}
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
if (this.collectionsType.type === "my-collections") {
this.$store.commit("postwoman/removeRequest", {
collectionIndex,
folderName,
requestIndex,
flag: "rest",
})
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
this.syncCollections()
} else if (this.collectionsType.type === "team-collections") {
teamUtils
.deleteRequest(this.$apollo, requestIndex)
.then(() => {
// Result
this.$toast.success(this.$t("deleted"), {
icon: "delete",
})
})
.catch((error) => {
// Error
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
}
},
},
}
</script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@@ -10,43 +13,73 @@
@dragend="dragging = false"
>
<button class="icon" @click="toggleShowChildren">
<i class="material-icons" v-show="!showChildren && !isFiltered">arrow_right</i>
<i class="material-icons" v-show="showChildren || isFiltered">arrow_drop_down</i>
<i class="material-icons">folder</i>
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i v-if="isSelected" class="text-green-400 material-icons"
>check_circle</i
>
<i v-else class="material-icons">folder</i>
<span>{{ collection.name }}</span>
</button>
<div>
<button
v-if="doc"
v-if="doc && !selected"
v-tooltip.left="$t('import')"
class="icon"
@click="$emit('select-collection')"
v-tooltip.left="$t('import')"
>
<i class="material-icons">topic</i>
<i class="material-icons">check_box_outline_blank</i>
</button>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button
v-if="doc && selected"
v-tooltip.left="$t('delete')"
class="icon"
@click="$emit('unselect-collection')"
>
<i class="material-icons">check_box</i>
</button>
<v-popover v-if="!saveRequest">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
class="icon"
@click="$emit('add-folder', { folder: collection, path: `${collectionIndex}` })"
v-close-popover
class="icon"
@click="
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
</button>
</div>
<div>
<button class="icon" @click="$emit('edit-collection')" v-close-popover>
<button
v-close-popover
class="icon"
@click="$emit('edit-collection')"
>
<i class="material-icons">create</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button
v-close-popover
class="icon"
@click="confirmRemove = true"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -59,19 +92,24 @@
<ul class="flex-col">
<li
v-for="(folder, index) in collection.folders"
:key="folder.name"
:key="index"
class="ml-8 border-l border-brdColor"
>
<CollectionsFolder
<CollectionsMyFolder
:folder="folder"
:folder-index="index"
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:doc="doc"
:isFiltered="isFiltered"
:save-request="saveRequest"
:collections-type="collectionsType"
:is-filtered="isFiltered"
:picked="picked"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
@@ -81,24 +119,36 @@
:key="index"
class="ml-8 border-l border-brdColor"
>
<CollectionsRequest
<CollectionsMyRequest
:request="request"
:collection-index="collectionIndex"
:folder-index="-1"
:folder-name="collection.name"
:folder-path="collectionIndex.toString()"
:request-index="index"
:doc="doc"
@edit-request="$emit('edit-request', $event)"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
@edit-request="editRequest($event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
<ul>
<li
v-if="collection.folders.length === 0 && collection.requests.length === 0"
v-if="
(collection.folders == undefined ||
collection.folders.length === 0) &&
(collection.requests == undefined ||
collection.requests.length === 0)
"
class="flex ml-8 border-l border-brdColor"
>
<p class="info">
<i class="material-icons">not_interested</i> {{ $t("collection_empty") }}
<i class="material-icons">not_interested</i>
{{ $t("collection_empty") }}
</p>
</li>
</ul>
@@ -118,10 +168,14 @@ import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
collectionIndex: Number,
collection: Object,
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => {} },
doc: Boolean,
isFiltered: Boolean,
selected: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
data() {
return {
@@ -129,6 +183,9 @@ export default {
dragging: false,
selectedFolder: {},
confirmRemove: false,
prevCursor: "",
cursor: "",
pageNo: 0,
}
},
subscriptions() {
@@ -136,7 +193,19 @@ export default {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "my-collection" &&
this.picked.collectionIndex === this.collectionIndex
)
},
},
methods: {
editRequest(event) {
this.$emit("edit-request", event)
},
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
@@ -146,17 +215,24 @@ export default {
}
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "my-collection",
collectionIndex: this.collectionIndex,
},
})
this.$emit("expand-collection", this.collection.id)
this.showChildren = !this.showChildren
},
removeCollection() {
this.$store.commit("postwoman/removeCollection", {
this.$emit("remove-collection", {
collectionsType: this.collectionsType,
collectionIndex: this.collectionIndex,
flag: "rest",
collectionID: this.collection.id,
})
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
this.syncCollections()
this.confirmRemove = false
},
dropEvent({ dataTransfer }) {
this.dragging = !this.dragging
@@ -177,6 +253,13 @@ export default {
})
this.syncCollections()
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
}
</script>

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@@ -11,22 +14,29 @@
>
<div>
<button class="icon" @click="toggleShowChildren">
<i class="material-icons" v-show="!showChildren && !isFiltered">arrow_right</i>
<i class="material-icons" v-show="showChildren || isFiltered">arrow_drop_down</i>
<i class="material-icons">folder_open</i>
<span>{{ folder.name }}</span>
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i v-if="isSelected" class="text-green-400 material-icons"
>check_circle</i
>
<i v-else class="material-icons">folder_open</i>
<span>{{ folder.name ? folder.name : folder.title }}</span>
</button>
</div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<v-popover v-if="!saveRequest">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
class="icon"
@click="$emit('add-folder', { folder, path: folderPath })"
v-close-popover
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
@@ -34,16 +44,18 @@
</div>
<div>
<button
class="icon"
@click="$emit('edit-folder', { folder, folderIndex, collectionIndex })"
v-close-popover
class="icon"
@click="
$emit('edit-folder', { folder, folderIndex, collectionIndex })
"
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -52,21 +64,27 @@
</v-popover>
</div>
<div v-show="showChildren || isFiltered">
<ul v-if="folder.folders && folder.folders.length" class="flex-col">
<ul class="flex-col">
<li
v-for="(subFolder, subFolderIndex) in folder.folders"
:key="subFolder.name"
class="ml-8 border-l border-brdColor"
>
<CollectionsFolder
<CollectionsMyFolder
:folder="subFolder"
:folder-index="subFolderIndex"
:collection-index="collectionIndex"
:doc="doc"
:save-request="saveRequest"
:collections-type="collectionsType"
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
@@ -76,14 +94,20 @@
:key="index"
class="flex ml-8 border-l border-brdColor"
>
<CollectionsRequest
<CollectionsMyRequest
:request="request"
:collection-index="collectionIndex"
:folder-index="folderIndex"
:folder-name="folder.name"
:request-index="index"
:folder-path="folderPath"
:doc="doc"
:picked="picked"
:save-request="saveRequest"
:collections-type="collectionsType"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
@@ -96,7 +120,10 @@
"
>
<li class="flex ml-8 border-l border-brdColor">
<p class="info"><i class="material-icons">not_interested</i> {{ $t("folder_empty") }}</p>
<p class="info">
<i class="material-icons">not_interested</i>
{{ $t("folder_empty") }}
</p>
</li>
</ul>
</div>
@@ -114,20 +141,25 @@ import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
name: "folder",
name: "Folder",
props: {
folder: Object,
folderIndex: Number,
collectionIndex: Number,
folderPath: String,
folder: { type: Object, default: () => {} },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
doc: Boolean,
saveRequest: Boolean,
isFiltered: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
data() {
return {
showChildren: false,
dragging: false,
confirmRemove: false,
prevCursor: "",
cursor: "",
}
},
subscriptions() {
@@ -135,6 +167,15 @@ export default {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "my-folder" &&
this.picked.folderPath === this.folderPath
)
},
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
@@ -145,6 +186,15 @@ export default {
}
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "my-folder",
collectionIndex: this.collectionIndex,
folderName: this.folder.name,
folderPath: this.folderPath,
},
})
this.showChildren = !this.showChildren
},
removeFolder() {
@@ -179,6 +229,13 @@ export default {
})
this.syncCollections()
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
}
</script>

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div
:class="['row-wrapper transition duration-150 ease-in-out', { 'bg-bgDarkColor': dragging }]"
:class="[
'row-wrapper transition duration-150 ease-in-out',
{ 'bg-bgDarkColor': dragging },
]"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@@ -10,21 +13,28 @@
>
<div>
<button
v-tooltip="!doc ? $t('use_request') : ''"
class="icon"
@click="!doc ? selectRequest() : {}"
v-tooltip="!doc ? $t('use_request') : ''"
>
<span :class="getRequestLabelColor(request.method)">{{ request.method }}</span>
<i v-if="isSelected" class="mx-3 text-green-400 material-icons"
>check_circle</i
>
<span v-else :class="getRequestLabelColor(request.method)">{{
request.method
}}</span>
<span>{{ request.name }}</span>
</button>
</div>
<v-popover>
<button class="tooltip-target icon" v-tooltip="$t('more')">
<v-popover v-if="!saveRequest">
<button v-tooltip="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
class="icon"
@click="
$emit('edit-request', {
@@ -35,14 +45,13 @@
requestIndex,
})
"
v-close-popover
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -60,17 +69,19 @@
</template>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
request: Object,
collectionIndex: Number,
folderIndex: Number,
folderName: String,
requestIndex: Number,
request: { type: Object, default: () => {} },
collectionIndex: { type: Number, default: null },
folderIndex: { type: Number, default: null },
folderName: { type: String, default: null },
// eslint-disable-next-line vue/require-default-prop
requestIndex: [Number, String],
doc: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
picked: { type: Object, default: () => {} },
},
data() {
return {
@@ -85,22 +96,30 @@ export default {
confirmRemove: false,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "my-request" &&
this.picked.folderPath === this.folderPath &&
this.picked.requestIndex === this.requestIndex
)
},
},
methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
selectRequest() {
this.$store.commit("postwoman/selectRequest", { request: this.request })
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: this.collectionIndex,
folderPath: this.folderPath,
folderName: this.folderName,
requestIndex: this.requestIndex,
},
})
else
this.$store.commit("postwoman/selectRequest", { request: this.request })
},
dragStart({ dataTransfer }) {
this.dragging = !this.dragging
@@ -110,20 +129,17 @@ export default {
dataTransfer.setData("requestIndex", this.$props.requestIndex)
},
removeRequest() {
this.$store.commit("postwoman/removeRequest", {
this.$emit("remove-request", {
collectionIndex: this.$props.collectionIndex,
folderName: this.$props.folderName,
requestIndex: this.$props.requestIndex,
flag: "rest",
})
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
this.confirmRemove = false
this.syncCollections()
},
getRequestLabelColor(method) {
return this.requestMethodLabels[method.toLowerCase()] || this.requestMethodLabels.default
return (
this.requestMethodLabels[method.toLowerCase()] ||
this.requestMethodLabels.default
)
},
},
}

View File

@@ -0,0 +1,236 @@
<template>
<div>
<div class="transition duration-150 ease-in-out row-wrapper">
<button class="icon" @click="toggleShowChildren">
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i v-if="isSelected" class="text-green-400 material-icons"
>check_circle</i
>
<i v-else class="material-icons">folder</i>
<span>{{ collection.title }}</span>
</button>
<div>
<button
v-if="doc && !selected"
v-tooltip.left="$t('import')"
class="icon"
@click="$emit('select-collection')"
>
<i class="material-icons">check_box_outline_blank</i>
</button>
<button
v-if="doc && selected"
v-tooltip.left="$t('delete')"
class="icon"
@click="$emit('unselect-collection')"
>
<i class="material-icons">check_box</i>
</button>
<v-popover v-if="!saveRequest">
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tooltip.left="$t('more')"
class="tooltip-target icon"
>
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
</button>
</div>
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="$emit('edit-collection')"
>
<i class="material-icons">create</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="confirmRemove = true"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
</div>
<div v-show="showChildren || isFiltered">
<ul class="flex-col">
<li
v-for="(folder, index) in collection.children"
:key="folder.title"
class="ml-8 border-l border-brdColor"
>
<CollectionsTeamsFolder
:folder="folder"
:folder-index="index"
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:doc="doc"
:save-request="saveRequest"
:collections-type="collectionsType"
:is-filtered="isFiltered"
:picked="picked"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="removeRequest"
/>
</li>
</ul>
<ul class="flex-col">
<li
v-for="(request, index) in collection.requests"
:key="index"
class="ml-8 border-l border-brdColor"
>
<CollectionsTeamsRequest
:request="request.request"
:collection-index="collectionIndex"
:folder-index="-1"
:folder-name="collection.name"
:request-index="request.id"
:doc="doc"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
@edit-request="editRequest($event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
<ul>
<li
v-if="
(collection.children == undefined ||
collection.children.length === 0) &&
(collection.requests == undefined ||
collection.requests.length === 0)
"
class="flex ml-8 border-l border-brdColor"
>
<p class="info">
<i class="material-icons">not_interested</i>
{{ $t("collection_empty") }}
</p>
</li>
</ul>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('are_you_sure_remove_collection')"
@hide-modal="confirmRemove = false"
@resolve="removeCollection"
/>
</div>
</template>
<script>
export default {
props: {
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => {} },
doc: Boolean,
isFiltered: Boolean,
selected: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
data() {
return {
showChildren: false,
dragging: false,
selectedFolder: {},
confirmRemove: false,
prevCursor: "",
cursor: "",
pageNo: 0,
}
},
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "teams-collection" &&
this.picked.collectionID === this.collection.id
)
},
},
methods: {
editRequest(event) {
this.$emit("edit-request", event)
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-collection",
collectionID: this.collection.id,
},
})
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-collection",
collectionID: this.collection.id,
},
})
this.$emit("expand-collection", this.collection.id)
this.showChildren = !this.showChildren
},
removeCollection() {
this.$emit("remove-collection", {
collectionsType: this.collectionsType,
collectionIndex: this.collectionIndex,
collectionID: this.collection.id,
})
this.confirmRemove = false
},
expandCollection(collectionID) {
this.$emit("expand-collection", collectionID)
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
}
</script>

View File

@@ -0,0 +1,219 @@
<template>
<div>
<div class="transition duration-150 ease-in-out row-wrapper">
<div>
<button class="icon" @click="toggleShowChildren">
<i v-show="!showChildren && !isFiltered" class="material-icons"
>arrow_right</i
>
<i v-show="showChildren || isFiltered" class="material-icons"
>arrow_drop_down</i
>
<i v-if="isSelected" class="text-green-400 material-icons"
>check_circle</i
>
<i v-else class="material-icons">folder_open</i>
<span>{{ folder.name ? folder.name : folder.title }}</span>
</button>
</div>
<v-popover v-if="!saveRequest">
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tooltip.left="$t('more')"
class="tooltip-target icon"
>
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="$emit('add-folder', { folder, path: folderPath })"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("new_folder") }}</span>
</button>
</div>
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="
$emit('edit-folder', { folder, folderIndex, collectionIndex })
"
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-close-popover
class="icon"
@click="confirmRemove = true"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<div v-show="showChildren || isFiltered">
<ul class="flex-col">
<li
v-for="(subFolder, subFolderIndex) in folder.children"
:key="subFolder.name"
class="ml-8 border-l border-brdColor"
>
<CollectionsTeamsFolder
:folder="subFolder"
:folder-index="subFolderIndex"
:collection-index="collectionIndex"
:doc="doc"
:save-request="saveRequest"
:collections-type="collectionsType"
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="removeRequest"
/>
</li>
</ul>
<ul class="flex-col">
<li
v-for="(request, index) in folder.requests"
:key="index"
class="flex ml-8 border-l border-brdColor"
>
<CollectionsTeamsRequest
:request="request.request"
:collection-index="collectionIndex"
:folder-index="folderIndex"
:folder-name="folder.name"
:request-index="request.id"
:doc="doc"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
/>
</li>
</ul>
<ul
v-if="
(folder.children == undefined || folder.children.length === 0) &&
(folder.requests == undefined || folder.requests.length === 0)
"
>
<li class="flex ml-8 border-l border-brdColor">
<p class="info">
<i class="material-icons">not_interested</i>
{{ $t("folder_empty") }}
</p>
</li>
</ul>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('are_you_sure_remove_folder')"
@hide-modal="confirmRemove = false"
@resolve="removeFolder"
/>
</div>
</template>
<script>
import * as teamUtils from "~/helpers/teams/utils"
export default {
name: "Folder",
props: {
folder: { type: Object, default: () => {} },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
doc: Boolean,
saveRequest: Boolean,
isFiltered: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
data() {
return {
showChildren: false,
confirmRemove: false,
prevCursor: "",
cursor: "",
}
},
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "teams-folder" &&
this.picked.folderID === this.folder.id
)
},
},
methods: {
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-folder",
folderID: this.folder.id,
},
})
this.$emit("expand-collection", this.$props.folder.id)
this.showChildren = !this.showChildren
},
removeFolder() {
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
teamUtils
.deleteCollection(this.$apollo, this.folder.id)
.then(() => {
// Result
this.$toast.success(this.$t("deleted"), {
icon: "delete",
})
this.$emit("update-team-collections")
this.confirmRemove = false
})
.catch((error) => {
// Error
this.$toast.error(this.$t("error_occurred"), {
icon: "done",
})
console.error(error)
})
this.$emit("update-team-collections")
}
},
expandCollection(collectionID) {
this.$emit("expand-collection", collectionID)
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
}
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div>
<div class="transition duration-150 ease-in-out row-wrapper">
<div>
<button
v-tooltip="!doc ? $t('use_request') : ''"
class="icon"
@click="!doc ? selectRequest() : {}"
>
<i v-if="isSelected" class="mx-3 text-green-400 material-icons"
>check_circle</i
>
<span v-else :class="getRequestLabelColor(request.method)">{{
request.method
}}</span>
<span>{{ request.name }}</span>
</button>
</div>
<v-popover v-if="!saveRequest">
<button
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tooltip="$t('more')"
class="tooltip-target icon"
>
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
class="icon"
@click="
$emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
})
"
>
<i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('are_you_sure_remove_request')"
@hide-modal="confirmRemove = false"
@resolve="removeRequest"
/>
</div>
</template>
<script>
export default {
props: {
request: { type: Object, default: () => {} },
collectionIndex: { type: Number, default: null },
folderIndex: { type: Number, default: null },
folderName: { type: String, default: null },
// eslint-disable-next-line vue/require-default-prop
requestIndex: [Number, String],
doc: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
data() {
return {
dragging: false,
requestMethodLabels: {
get: "text-green-400",
post: "text-yellow-400",
put: "text-blue-400",
delete: "text-red-400",
default: "text-gray-400",
},
confirmRemove: false,
}
},
computed: {
isSelected() {
return (
this.picked &&
this.picked.pickedType === "teams-request" &&
this.picked.requestID === this.requestIndex
)
},
},
methods: {
selectRequest() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-request",
requestID: this.requestIndex,
},
})
else
this.$store.commit("postwoman/selectRequest", { request: this.request })
},
removeRequest() {
this.$emit("remove-request", {
collectionIndex: this.$props.collectionIndex,
folderName: this.$props.folderName,
requestIndex: this.$props.requestIndex,
})
},
getRequestLabelColor(method) {
return (
this.requestMethodLabels[method.toLowerCase()] ||
this.requestMethodLabels.default
)
},
},
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div class="collection">
<h2>
<i class="material-icons">folder</i>
{{ collection.name || $t("none") }}
</h2>
<span
v-for="(folder, index) in collection.folders"
:key="`sub-collection-${index}`"
class="folder"
>
<DocsFolder :folder="folder" />
</span>
<div
v-for="(request, index) in collection.requests"
:key="`request-${index}`"
>
<DocsRequest :request="request" />
</div>
</div>
</template>
<script>
export default {
props: {
collection: { type: Object, default: () => {} },
},
}
</script>
<style lang="scss" scoped>
.collection {
@apply flex;
@apply flex-col;
@apply justify-center;
@apply flex-1;
@apply p-4;
.material-icons {
@apply mr-4;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="folder">
<h3>
<i class="material-icons">folder_open</i>
{{ folder.name || $t("none") }}
</h3>
<div v-for="(subFolder, index) in folder.folders" :key="index">
<DocsFolder :folder="subFolder" />
</div>
<div v-for="(request, index) in folder.requests" :key="index">
<DocsRequest :request="request" />
</div>
</div>
</template>
<script>
export default {
props: {
folder: { type: Object, default: () => {} },
},
}
</script>
<style lang="scss" scoped>
.folder {
@apply flex;
@apply flex-col;
@apply justify-center;
@apply flex-1;
@apply p-4;
@apply border-l;
@apply border-brdColor;
@apply mt-4;
.material-icons {
@apply mr-4;
}
}
</style>

153
components/docs/Request.vue Normal file
View File

@@ -0,0 +1,153 @@
<template>
<div class="request">
<h4>
<i class="material-icons">insert_drive_file</i>
{{ request.name || $t("none") }}
</h4>
<p v-if="request.url" class="doc-desc">
<span>
{{ $t("url") }}: <code>{{ request.url || $t("none") }}</code>
</span>
</p>
<p v-if="request.path" class="doc-desc">
<span>
{{ $t("path") }}:
<code>{{ request.path || $t("none") }}</code>
</span>
</p>
<p v-if="request.method" class="doc-desc">
<span>
{{ $t("method") }}:
<code>{{ request.method || $t("none") }}</code>
</span>
</p>
<p v-if="request.auth" class="doc-desc">
<span>
{{ $t("authentication") }}:
<code>{{ request.auth || $t("none") }}</code>
</span>
</p>
<p v-if="request.httpUser" class="doc-desc">
<span>
{{ $t("username") }}:
<code>{{ request.httpUser || $t("none") }}</code>
</span>
</p>
<p v-if="request.httpPassword" class="doc-desc">
<span>
{{ $t("password") }}:
<code>{{ request.httpPassword || $t("none") }}</code>
</span>
</p>
<p v-if="request.bearerToken" class="doc-desc">
<span>
{{ $t("token") }}:
<code>{{ request.bearerToken || $t("none") }}</code>
</span>
</p>
<h4 v-if="request.headers">{{ $t("headers") }}</h4>
<span v-if="request.headers">
<p v-for="header in request.headers" :key="header.key" class="doc-desc">
<span>
{{ header.key || $t("none") }}:
<code>{{ header.value || $t("none") }}</code>
</span>
</p>
</span>
<h4 v-if="request.params">{{ $t("parameters") }}</h4>
<span v-if="request.params">
<p
v-for="parameter in request.params"
:key="parameter.key"
class="doc-desc"
>
<span>
{{ parameter.key || $t("none") }}:
<code>{{ parameter.value || $t("none") }}</code>
</span>
</p>
</span>
<h4 v-if="request.bodyParams">{{ $t("payload") }}</h4>
<span v-if="request.bodyParams">
<p
v-for="payload in request.bodyParams"
:key="payload.key"
class="doc-desc"
>
<span>
{{ payload.key || $t("none") }}:
<code>{{ payload.value || $t("none") }}</code>
</span>
</p>
</span>
<p v-if="request.rawParams" class="doc-desc">
<span>
{{ $t("parameters") }}:
<code>{{ request.rawParams || $t("none") }}</code>
</span>
</p>
<p v-if="request.contentType" class="doc-desc">
<span>
{{ $t("content_type") }}:
<code>{{ request.contentType || $t("none") }}</code>
</span>
</p>
<p v-if="request.requestType" class="doc-desc">
<span>
{{ $t("request_type") }}:
<code>{{ request.requestType || $t("none") }}</code>
</span>
</p>
</div>
</template>
<script>
export default {
props: {
request: { type: Object, default: () => {} },
},
}
</script>
<style scoped lang="scss">
.request {
@apply flex;
@apply flex-col;
@apply justify-center;
@apply flex-1;
@apply p-4;
@apply border;
@apply border-brdColor;
@apply rounded-lg;
@apply mt-4;
h4 {
@apply mt-4;
}
.material-icons {
@apply mr-4;
}
}
.doc-desc {
@apply flex;
@apply flex-col;
@apply justify-center;
@apply flex-1;
@apply p-4;
@apply text-fgLightColor;
@apply border-b;
@apply border-dashed;
@apply border-brdColor;
@apply m-0;
&:last-child {
@apply border-b-0;
}
.material-icons {
@apply mr-4;
}
}
</style>

View File

@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="$t('my_new_environment')"
@keyup.enter="addNewEnvironment"
/>
@@ -46,18 +46,20 @@ export default {
},
data() {
return {
name: undefined,
name: null,
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
fb.writeEnvironments(
JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))
)
}
},
addNewEnvironment() {
@@ -65,7 +67,7 @@ export default {
this.$toast.info(this.$t("invalid_environment_name"))
return
}
let newEnvironment = [
const newEnvironment = [
{
name: this.$data.name,
variables: [],
@@ -75,12 +77,12 @@ export default {
environments: newEnvironment,
confirmation: "Environment added",
})
this.$emit("hide-modal")
this.syncEnvironments()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
this.$data.name = undefined
},
},
}

View File

@@ -13,24 +13,35 @@
<div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label>
<input
type="text"
id="selectLabel"
v-model="name"
type="text"
:placeholder="editingEnvironment.name"
@keyup.enter="saveEnvironment"
/>
<div class="row-wrapper">
<label for="variableList">{{ $t("env_variable_list") }}</label>
<div>
<button class="icon" @click="clearContent($event)" v-tooltip.bottom="$t('clear')">
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent($event)"
>
<i class="material-icons">clear_all</i>
</button>
</div>
</div>
<ul
v-for="(variable, index) in this.editingEnvCopy.variables"
v-for="(variable, index) in editingEnvCopy.variables"
:key="index"
class="border-b border-dashed divide-y md:divide-x border-brdColor divide-dashed divide-brdColor md:divide-y-0"
class="
border-b border-dashed
divide-y
md:divide-x
border-brdColor
divide-dashed divide-brdColor
md:divide-y-0
"
:class="{ 'border-t': index == 0 }"
>
<li>
@@ -38,13 +49,13 @@
:placeholder="$t('variable_count', { count: index + 1 })"
:name="'param' + index"
:value="variable.key"
autofocus
@change="
$store.commit('postwoman/setVariableKey', {
index,
value: $event.target.value,
})
"
autofocus
/>
</li>
<li>
@@ -52,7 +63,9 @@
:placeholder="$t('value_count', { count: index + 1 })"
:name="'value' + index"
:value="
typeof variable.value === 'string' ? variable.value : JSON.stringify(variable.value)
typeof variable.value === 'string'
? variable.value
: JSON.stringify(variable.value)
"
@change="
$store.commit('postwoman/setVariableValue', {
@@ -65,10 +78,10 @@
<div>
<li>
<button
id="variable"
v-tooltip.bottom="$t('delete')"
class="icon"
@click="removeEnvironmentVariable(index)"
v-tooltip.bottom="$t('delete')"
id="variable"
>
<i class="material-icons">delete</i>
</button>
@@ -107,29 +120,20 @@ import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
editingEnvironment: Object,
editingEnvironmentIndex: Number,
editingEnvironment: { type: Object, default: () => {} },
editingEnvironmentIndex: { type: Number, default: null },
},
data() {
return {
name: undefined,
name: null,
doneButton: '<i class="material-icons">done</i>',
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
watch: {
editingEnvironment(update) {
this.name =
this.$props.editingEnvironment && this.$props.editingEnvironment.name
? this.$props.editingEnvironment.name
: undefined
this.$store.commit("postwoman/setEditingEnvironment", this.$props.editingEnvironment)
},
},
computed: {
editingEnvCopy() {
return this.$store.state.postwoman.editingEnvironment
@@ -139,10 +143,24 @@ export default {
return result === "" ? "" : JSON.stringify(result)
},
},
watch: {
editingEnvironment() {
this.name =
this.$props.editingEnvironment && this.$props.editingEnvironment.name
? this.$props.editingEnvironment.name
: undefined
this.$store.commit(
"postwoman/setEditingEnvironment",
this.$props.editingEnvironment
)
},
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
fb.writeEnvironments(
JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))
)
}
},
clearContent({ target }) {
@@ -151,18 +169,21 @@ export default {
this.$toast.info(this.$t("cleared"), {
icon: "clear_all",
})
setTimeout(() => (target.innerHTML = '<i class="material-icons">clear_all</i>'), 1000)
setTimeout(
() => (target.innerHTML = '<i class="material-icons">clear_all</i>'),
1000
)
},
addEnvironmentVariable() {
let value = { key: "", value: "" }
const value = { key: "", value: "" }
this.$store.commit("postwoman/addVariable", value)
this.syncEnvironments()
},
removeEnvironmentVariable(index) {
let variableIndex = index
const variableIndex = index
const oldVariables = this.editingEnvCopy.variables.slice()
const newVariables = this.editingEnvCopy.variables.filter(
(variable, index) => variableIndex !== index
(_, index) => variableIndex !== index
)
this.$store.commit("postwoman/removeVariable", newVariables)
@@ -170,7 +191,7 @@ export default {
icon: "delete",
action: {
text: this.$t("undo"),
onClick: (e, toastObject) => {
onClick: (_, toastObject) => {
this.$store.commit("postwoman/removeVariable", oldVariables)
toastObject.remove()
},
@@ -191,12 +212,12 @@ export default {
environment: environmentUpdated,
environmentIndex: this.$props.editingEnvironmentIndex,
})
this.$emit("hide-modal")
this.syncEnvironments()
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
this.$data.name = undefined
},
},
}

View File

@@ -8,18 +8,22 @@
</button>
</div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="$emit('edit-environment')" v-close-popover>
<button
v-close-popover
class="icon"
@click="$emit('edit-environment')"
>
<i class="material-icons">create</i>
<span>{{ $t("edit") }}</span>
</button>
</div>
<div>
<button class="icon" @click="confirmRemove = true" v-close-popover>
<button v-close-popover class="icon" @click="confirmRemove = true">
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
@@ -42,8 +46,8 @@ import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
environment: Object,
environmentIndex: Number,
environment: { type: Object, default: () => {} },
environmentIndex: { type: Number, default: null },
},
data() {
return {
@@ -52,13 +56,15 @@ export default {
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
fb.writeEnvironments(
JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))
)
}
},
removeEnvironment() {

View File

@@ -2,15 +2,21 @@
<SmartModal v-if="show" @close="hideModal">
<div slot="header">
<div class="row-wrapper">
<h3 class="title">{{ $t("import_export") }} {{ $t("environments") }}</h3>
<h3 class="title">
{{ $t("import_export") }} {{ $t("environments") }}
</h3>
<div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<button v-tooltip.left="$t('more')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="readEnvironmentGist" v-close-popover>
<button
v-close-popover
class="icon"
@click="readEnvironmentGist"
>
<i class="material-icons">assignment_returned</i>
<span>{{ $t("import_from_gist") }}</span>
</button>
@@ -25,12 +31,16 @@
}"
>
<button
v-close-popover
:disabled="
!fb.currentUser ? true : fb.currentUser.provider !== 'github.com' ? true : false
!fb.currentUser
? true
: fb.currentUser.provider !== 'github.com'
? true
: false
"
class="icon"
@click="createEnvironmentGist"
v-close-popover
>
<i class="material-icons">assignment_turned_in</i>
<span>{{ $t("create_secret_gist") }}</span>
@@ -48,42 +58,48 @@
<div class="flex flex-col items-start p-2">
<span
v-tooltip="{
content: !fb.currentUser ? $t('login_first') : $t('replace_current'),
content: !fb.currentUser
? $t('login_first')
: $t('replace_current'),
}"
>
<button :disabled="!fb.currentUser" class="icon" @click="syncEnvironments">
<button
:disabled="!fb.currentUser"
class="icon"
@click="syncEnvironments"
>
<i class="material-icons">folder_shared</i>
<span>{{ $t("import_from_sync") }}</span>
</button>
</span>
<button
v-tooltip="$t('replace_current')"
class="icon"
@click="openDialogChooseFileToReplaceWith"
v-tooltip="$t('replace_current')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("replace_json") }}</span>
<input
type="file"
@change="replaceWithJSON"
style="display: none"
ref="inputChooseFileToReplaceWith"
type="file"
style="display: none"
accept="application/json"
@change="replaceWithJSON"
/>
</button>
<button
v-tooltip="$t('preserve_current')"
class="icon"
@click="openDialogChooseFileToImportFrom"
v-tooltip="$t('preserve_current')"
>
<i class="material-icons">folder_special</i>
<span>{{ $t("import_json") }}</span>
<input
type="file"
@change="importFromJSON"
style="display: none"
ref="inputChooseFileToImportFrom"
type="file"
style="display: none"
accept="application/json"
@change="importFromJSON"
/>
</button>
</div>
@@ -102,7 +118,11 @@
<button class="icon" @click="hideModal">
{{ $t("cancel") }}
</button>
<button class="icon primary" @click="exportJSON" v-tooltip="$t('download_file')">
<button
v-tooltip="$t('download_file')"
class="icon primary"
@click="exportJSON"
>
{{ $t("export") }}
</button>
</span>
@@ -116,6 +136,9 @@ import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
show: Boolean,
},
data() {
return {
fb,
@@ -124,12 +147,9 @@ export default {
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
props: {
show: Boolean,
},
computed: {
environmentJson() {
return JSON.stringify(this.$store.state.postwoman.environments, null, 2)
@@ -154,11 +174,11 @@ export default {
},
}
)
.then(({ html_url }) => {
.then((res) => {
this.$toast.success(this.$t("gist_created"), {
icon: "done",
})
window.open(html_url)
window.open(res.html_url)
})
.catch((error) => {
this.$toast.error(this.$t("something_went_wrong"), {
@@ -168,7 +188,7 @@ export default {
})
},
async readEnvironmentGist() {
let gist = prompt(this.$t("enter_gist_url"))
const gist = prompt(this.$t("enter_gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
@@ -177,7 +197,7 @@ export default {
},
})
.then(({ files }) => {
let environments = JSON.parse(Object.values(files)[0].content)
const environments = JSON.parse(Object.values(files)[0].content)
this.$store.commit("postwoman/replaceEnvironments", environments)
this.fileImported()
this.syncToFBEnvironments()
@@ -197,10 +217,10 @@ export default {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
let environments = JSON.parse(content)
const content = target.result
const environments = JSON.parse(content)
this.$store.commit("postwoman/replaceEnvironments", environments)
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
@@ -209,13 +229,13 @@ export default {
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
let reader = new FileReader()
const reader = new FileReader()
reader.onload = ({ target }) => {
let content = target.result
let importFileObj = JSON.parse(content)
const content = target.result
const importFileObj = JSON.parse(content)
if (
importFileObj["_postman_variable_scope"] === "environment" ||
importFileObj["_postman_variable_scope"] === "globals"
importFileObj._postman_variable_scope === "environment" ||
importFileObj._postman_variable_scope === "globals"
) {
this.importFromPostman(importFileObj)
} else {
@@ -227,25 +247,27 @@ export default {
this.$refs.inputChooseFileToImportFrom.value = ""
},
importFromPostwoman(environments) {
let confirmation = this.$t("file_imported")
const confirmation = this.$t("file_imported")
this.$store.commit("postwoman/importAddEnvironments", {
environments,
confirmation,
})
},
importFromPostman({ name, values }) {
let environment = { name, variables: [] }
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
let environments = [environment]
const environment = { name, variables: [] }
values.forEach(({ key, value }) =>
environment.variables.push({ key, value })
)
const environments = [environment]
this.importFromPostwoman(environments)
},
exportJSON() {
let text = this.environmentJson
text = text.replace(/\n/g, "\r\n")
let blob = new Blob([text], {
const blob = new Blob([text], {
type: "text/json",
})
let anchor = document.createElement("a")
const anchor = document.createElement("a")
anchor.download = "hoppscotch-environment.json"
anchor.href = window.URL.createObjectURL(blob)
anchor.target = "_blank"
@@ -258,12 +280,17 @@ export default {
})
},
syncEnvironments() {
this.$store.commit("postwoman/replaceEnvironments", fb.currentEnvironments)
this.$store.commit(
"postwoman/replaceEnvironments",
fb.currentEnvironments
)
this.fileImported()
},
syncToFBEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
fb.writeEnvironments(
JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))
)
}
},
fileImported() {

View File

@@ -1,5 +1,10 @@
<template>
<AppSection icon="history" :label="$t('environments')" ref="environments" no-legend>
<AppSection
ref="environments"
icon="history"
:label="$t('environments')"
no-legend
>
<div class="show-on-large-screen">
<span class="select-wrapper">
<select
@@ -11,17 +16,24 @@
<option v-if="environments.length === 0" value="0">
{{ $t("create_new_environment") }}
</option>
<option v-for="(environment, index) in environments" :value="index" :key="index">
<option
v-for="(environment, index) in environments"
:key="index"
:value="index"
>
{{ environment.name }}
</option>
</select>
</span>
</div>
<EnvironmentsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
<EnvironmentsAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
/>
<EnvironmentsEdit
:show="showModalEdit"
:editingEnvironment="editingEnvironment"
:editingEnvironmentIndex="editingEnvironmentIndex"
:editing-environment="editingEnvironment"
:editing-environment-index="editingEnvironmentIndex"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
@@ -42,13 +54,17 @@
</div>
</div>
<p v-if="environments.length === 0" class="info">
<i class="material-icons">help_outline</i> {{ $t("create_new_environment") }}
<i class="material-icons">help_outline</i>
{{ $t("create_new_environment") }}
</p>
<div class="virtual-list">
<ul class="flex-col">
<li v-for="(environment, index) in environments" :key="environment.name">
<li
v-for="(environment, index) in environments"
:key="environment.name"
>
<EnvironmentsEnvironment
:environmentIndex="index"
:environment-index="index"
:environment="environment"
@edit-environment="editEnvironment(environment, index)"
/>
@@ -58,12 +74,6 @@
</AppSection>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
@@ -116,26 +126,30 @@ export default {
environment: this.defaultEnvironment,
environments: this.environments,
})
} else {
if (this.environments[this.selectedEnvironmentIndex])
this.$emit("use-environment", {
environment: this.environments[this.selectedEnvironmentIndex],
environments: this.environments,
})
else this.selectedEnvironmentIndex = -1
}
} else if (this.environments[this.selectedEnvironmentIndex])
this.$emit("use-environment", {
environment: this.environments[this.selectedEnvironmentIndex],
environments: this.environments,
})
else this.selectedEnvironmentIndex = -1
},
},
},
async mounted() {
mounted() {
this._keyListener = function (e) {
if (e.key === "Escape") {
e.preventDefault()
this.showModalImportExport = this.showModalAdd = this.showModalEdit = false
this.showModalImportExport =
this.showModalAdd =
this.showModalEdit =
false
}
}
document.addEventListener("keydown", this._keyListener.bind(this))
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
@@ -160,12 +174,17 @@ export default {
},
syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
fb.writeEnvironments(
JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))
)
}
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
}
</script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>

View File

@@ -28,19 +28,6 @@
</ul>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
.clamb-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
@apply overflow-hidden;
}
</style>
<script>
import { fb } from "~/helpers/fb"
@@ -60,3 +47,16 @@ export default {
},
}
</script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
.clamb-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
@apply overflow-hidden;
}
</style>

View File

@@ -2,25 +2,30 @@
<div class="flex-col">
<div class="show-on-large-screen">
<input
v-model="message"
:aria-label="$t('label')"
type="text"
autofocus
v-model="message"
:placeholder="$t('paste_a_note')"
@keyup.enter="formPost"
class="rounded-t-lg"
@keyup.enter="formPost"
/>
</div>
<div class="border-b show-on-large-screen border-brdColor">
<input
v-model="label"
:aria-label="$t('label')"
type="text"
autofocus
v-model="label"
:placeholder="$t('label')"
@keyup.enter="formPost"
/>
<button class="icon" :disabled="!(this.message || this.label)" value="Save" @click="formPost">
<button
class="icon"
:disabled="!(message || label)"
value="Save"
@click="formPost"
>
<i class="material-icons">add</i>
<span>Add</span>
</button>

View File

@@ -1,8 +1,13 @@
<template>
<div>
<div>
<button class="icon" @click="signInWithGoogle" v-close-popover>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons">
<button v-close-popover class="icon" @click="signInWithGoogle">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path
d="M12.24 10.285V14.4h6.806c-.275 1.765-2.056 5.174-6.806 5.174-4.095 0-7.439-3.389-7.439-7.574s3.345-7.574 7.439-7.574c2.33 0 3.891.989 4.785 1.849l3.254-3.138C18.189 1.186 15.479 0 12.24 0c-6.635 0-12 5.365-12 12s5.365 12 12 12c6.926 0 11.52-4.869 11.52-11.726 0-.788-.085-1.39-.189-1.989H12.24z"
/>
@@ -11,8 +16,13 @@
</button>
</div>
<div>
<button class="icon" @click="signInWithGithub" v-close-popover>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons">
<button v-close-popover class="icon" @click="signInWithGithub">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path
d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"
/>
@@ -49,7 +59,7 @@ export default {
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
onClick: (_, toastObject) => {
fb.writeSettings("syncHistory", true)
fb.writeSettings("syncCollections", true)
fb.writeSettings("syncEnvironments", true)
@@ -96,7 +106,7 @@ export default {
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: async (e, toastObject) => {
onClick: async (_, toastObject) => {
const { user } = await fb.signInWithGithub()
await user.linkAndRetrieveDataWithCredential(pendingCred)
@@ -111,7 +121,8 @@ export default {
},
async signInWithGithub() {
try {
const { credential, additionalUserInfo } = await fb.signInUserWithGithub()
const { credential, additionalUserInfo } =
await fb.signInUserWithGithub()
fb.setProviderInfo(credential.providerId, credential.accessToken)
@@ -122,7 +133,7 @@ export default {
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
onClick: (_, toastObject) => {
fb.writeSettings("syncHistory", true)
fb.writeSettings("syncCollections", true)
fb.writeSettings("syncEnvironments", true)
@@ -169,7 +180,7 @@ export default {
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: async (e, toastObject) => {
onClick: async (_, toastObject) => {
const { user } = await fb.signInUserWithGoogle()
await user.linkAndRetrieveDataWithCredential(pendingCred)

View File

@@ -1,6 +1,6 @@
<template>
<div>
<button class="icon" @click="logout" v-close-popover>
<button v-close-popover class="icon" @click="logout">
<i class="material-icons">exit_to_app</i>
<span>{{ $t("logout") }}</span>
</button>

View File

@@ -1,5 +1,7 @@
import feeds from "../Feeds"
import { shallowMount } from "@vue/test-utils"
import feeds from "../Feeds"
import { fb } from "~/helpers/fb"
jest.mock("~/helpers/fb", () => ({
__esModule: true,
@@ -27,8 +29,6 @@ jest.mock("~/helpers/fb", () => ({
},
}))
import { fb } from "~/helpers/fb"
const factory = () =>
shallowMount(feeds, {
mocks: {
@@ -53,7 +53,9 @@ describe("feeds", () => {
test("renders all the current feeds", () => {
const wrapper = factory()
expect(wrapper.findAll("div[data-test='list-item']").wrappers).toHaveLength(4)
expect(wrapper.findAll("div[data-test='list-item']").wrappers).toHaveLength(
4
)
})
test("feeds with no label displays the 'no_label' message", () => {
@@ -63,7 +65,7 @@ describe("feeds", () => {
wrapper
.findAll("label[data-test='list-label']")
.wrappers.map((e) => e.text())
.filter((text) => text == "no_label")
.filter((text) => text === "no_label")
).toHaveLength(2)
})
@@ -74,30 +76,28 @@ describe("feeds", () => {
wrapper
.findAll("li[data-test='list-message']")
.wrappers.map((e) => e.text())
.filter((text) => text == "empty")
.filter((text) => text === "empty")
).toHaveLength(2)
})
test("labels in the list are proper", () => {
const wrapper = factory()
expect(wrapper.findAll("label[data-test='list-label']").wrappers.map((e) => e.text())).toEqual([
"First",
"Second",
"no_label",
"no_label",
])
expect(
wrapper
.findAll("label[data-test='list-label']")
.wrappers.map((e) => e.text())
).toEqual(["First", "Second", "no_label", "no_label"])
})
test("messages in the list are proper", () => {
const wrapper = factory()
expect(wrapper.findAll("li[data-test='list-message']").wrappers.map((e) => e.text())).toEqual([
"First Message",
"empty",
"Third Message",
"empty",
])
expect(
wrapper
.findAll("li[data-test='list-message']")
.wrappers.map((e) => e.text())
).toEqual(["First Message", "empty", "Third Message", "empty"])
})
test("clicking on the delete button deletes the feed", async () => {

View File

@@ -1,5 +1,7 @@
import inputform from "../Inputform"
import { shallowMount } from "@vue/test-utils"
import inputform from "../Inputform"
import { fb } from "~/helpers/fb"
jest.mock("~/helpers/fb", () => ({
__esModule: true,
@@ -9,8 +11,6 @@ jest.mock("~/helpers/fb", () => ({
},
}))
import { fb } from "~/helpers/fb"
const factory = () =>
shallowMount(inputform, {
mocks: {

View File

@@ -1,5 +1,7 @@
import logout from "../Logout"
import { shallowMount, createLocalVue } from "@vue/test-utils"
import logout from "../Logout"
import { fb } from "~/helpers/fb"
jest.mock("~/helpers/fb", () => ({
__esModule: true,
@@ -9,8 +11,6 @@ jest.mock("~/helpers/fb", () => ({
},
}))
import { fb } from "~/helpers/fb"
const $toast = {
info: jest.fn(),
show: jest.fn(),
@@ -53,7 +53,9 @@ describe("logout", () => {
})
test("failed signout request fires a error toast", async () => {
fb.signOutUser.mockImplementationOnce(() => Promise.reject("test reject"))
fb.signOutUser.mockImplementationOnce(() =>
Promise.reject(new Error("test reject"))
)
const wrapper = factory()
const button = wrapper.find("button")

View File

@@ -6,19 +6,36 @@
(
<span v-for="(field, index) in fieldArgs" :key="index">
{{ field.name }}:
<GraphqlTypeLink :gqlType="field.type" :jumpTypeCallback="jumpTypeCallback" />
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
/>
<span v-if="index !== fieldArgs.length - 1"> , </span>
</span>
) </span
>:
<GraphqlTypeLink :gqlType="gqlField.type" :jumpTypeCallback="jumpTypeCallback" />
<GraphqlTypeLink
:gql-type="gqlField.type"
:jump-type-callback="jumpTypeCallback"
/>
</div>
<div class="mt-2 text-fgLightColor field-desc" v-if="gqlField.description">
<div v-if="gqlField.description" class="mt-2 text-fgLightColor field-desc">
{{ gqlField.description }}
</div>
<div
class="inline-block px-4 py-2 my-2 text-sm font-bold text-black bg-yellow-200 rounded-lg field-deprecated"
v-if="gqlField.isDeprecated"
class="
inline-block
px-4
py-2
my-2
text-sm
font-bold
text-black
bg-yellow-200
rounded-lg
field-deprecated
"
>
{{ $t("deprecated") }}
</div>
@@ -27,8 +44,14 @@
<div class="px-4 border-l-2 border-acColor">
<div v-for="(field, index) in fieldArgs" :key="index">
{{ field.name }}:
<GraphqlTypeLink :gqlType="field.type" :jumpTypeCallback="jumpTypeCallback" />
<div class="mt-2 text-fgLightColor field-desc" v-if="field.description">
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
/>
<div
v-if="field.description"
class="mt-2 text-fgLightColor field-desc"
>
{{ field.description }}
</div>
</div>
@@ -37,18 +60,11 @@
</div>
</template>
<style scoped lang="scss">
.field-highlighted {
@apply border-b-2;
@apply border-acColor;
}
</style>
<script>
export default {
props: {
gqlField: Object,
jumpTypeCallback: Function,
gqlField: { type: Object, default: () => {} },
jumpTypeCallback: { type: Function, default: () => {} },
isHighlighted: { type: Boolean, default: false },
},
computed: {
@@ -62,3 +78,10 @@ export default {
},
}
</script>
<style scoped lang="scss">
.field-highlighted {
@apply border-b-2;
@apply border-acColor;
}
</style>

View File

@@ -4,27 +4,15 @@
</div>
</template>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import * as gql from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import debounce from "~/helpers/utils/debounce"
export default {
@@ -44,7 +32,7 @@ export default {
},
options: {
type: Object,
default: {},
default: () => {},
},
styles: {
type: String,
@@ -84,7 +72,7 @@ export default {
mounted() {
defineGQLLanguageMode(ace)
let langTools = ace.require("ace/ext/language_tools")
const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/gql-query`,
@@ -108,12 +96,22 @@ export default {
})
const completer = {
getCompletions: (editor, _session, { row, column }, _prefix, callback) => {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.validationSchema) {
const completions = getAutocompleteSuggestions(this.validationSchema, editor.getValue(), {
line: row,
character: column,
})
const completions = getAutocompleteSuggestions(
this.validationSchema,
editor.getValue(),
{
line: row,
character: column,
}
)
callback(
null,
@@ -165,10 +163,14 @@ export default {
this.parseContents(this.value)
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
prettifyQuery() {
try {
this.value = gql.print(gql.parse(this.editor.getValue()))
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
} catch (e) {
this.$toast.error(`${this.$t("gql_prettify_invalid_query")}`, {
icon: "error",
@@ -180,9 +182,12 @@ export default {
if (this.theme) {
return this.theme
}
const strip = (str) => str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window.getComputedStyle(document.documentElement).getPropertyValue("--editor-theme")
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
@@ -198,12 +203,14 @@ export default {
if (this.validationSchema) {
this.editor.session.setAnnotations(
gql.validate(this.validationSchema, doc).map(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: message,
type: "error",
}))
gql
.validate(this.validationSchema, doc)
.map(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: message,
type: "error",
}))
)
}
} catch (e) {
@@ -221,9 +228,17 @@ export default {
}
}, 2000),
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,65 +1,79 @@
<template>
<div :id="`type_${gqlType.name}`" class="p-2 m-2">
<div class="font-bold type-title" :class="{ 'type-highlighted': isHighlighted }">
<div
class="font-bold type-title"
:class="{ 'type-highlighted': isHighlighted }"
>
<span v-if="isInput" class="font-normal text-acColor">input </span>
<span v-else-if="isInterface" class="font-normal text-acColor">interface </span>
<span v-else-if="isInterface" class="font-normal text-acColor"
>interface
</span>
<span v-else-if="isEnum" class="font-normal text-acColor">enum </span>
{{ gqlType.name }}
</div>
<div class="mt-2 text-fgLightColor type-desc" v-if="gqlType.description">
<div v-if="gqlType.description" class="mt-2 text-fgLightColor type-desc">
{{ gqlType.description }}
</div>
<div v-if="interfaces.length > 0" class="mb-2">
<h5>{{ $t("interfaces") }}</h5>
<div v-for="gqlInterface in interfaces" :key="gqlInterface.name" class="m-2 ml-4">
<GraphqlTypeLink :gqlType="gqlInterface" :jumpTypeCallback="jumpTypeCallback" />
<div
v-for="gqlInterface in interfaces"
:key="gqlInterface.name"
class="m-2 ml-4"
>
<GraphqlTypeLink
:gql-type="gqlInterface"
:jump-type-callback="jumpTypeCallback"
/>
</div>
</div>
<div v-if="children.length > 0" class="mb-2">
<h5>{{ $t("children") }}</h5>
<div v-for="child in children" :key="child.name" class="m-2 ml-4">
<GraphqlTypeLink :gqlType="child" :jumpTypeCallback="jumpTypeCallback" />
<GraphqlTypeLink
:gql-type="child"
:jump-type-callback="jumpTypeCallback"
/>
</div>
</div>
<div v-if="gqlType.getFields">
<h5>{{ $t("fields") }}</h5>
<div v-for="field in gqlType.getFields()" :key="field.name">
<GraphqlField
:gqlField="field"
:isHighlighted="isFieldHighlighted({ field })"
:jumpTypeCallback="jumpTypeCallback"
:gql-field="field"
:is-highlighted="isFieldHighlighted({ field })"
:jump-type-callback="jumpTypeCallback"
/>
</div>
</div>
<div v-if="isEnum">
<h5>{{ $t("values") }}</h5>
<div :key="value.name" v-for="value in gqlType.getValues()" class="m-4" v-text="value.name" />
<div
v-for="value in gqlType.getValues()"
:key="value.name"
class="m-4"
v-text="value.name"
></div>
</div>
</div>
</template>
<style scoped lang="scss">
.type-highlighted {
@apply text-acColor;
}
</style>
<script>
import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInterfaceType } from "graphql"
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
} from "graphql"
export default {
props: {
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
gqlType: {},
gqlTypes: Array,
jumpTypeCallback: Function,
gqlTypes: { type: Array, default: () => [] },
jumpTypeCallback: { type: Function, default: () => {} },
isHighlighted: { type: Boolean, default: false },
highlightedFields: { type: Array, default: () => [] },
},
methods: {
isFieldHighlighted({ field }) {
return !!this.highlightedFields.find(({ name }) => name === field.name)
},
},
computed: {
isInput() {
return this.gqlType instanceof GraphQLInputObjectType
@@ -75,9 +89,21 @@ export default {
},
children() {
return this.gqlTypes.filter(
(type) => type.getInterfaces && type.getInterfaces().includes(this.gqlType)
(type) =>
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
)
},
},
methods: {
isFieldHighlighted({ field }) {
return !!this.highlightedFields.find(({ name }) => name === field.name)
},
},
}
</script>
<style scoped lang="scss">
.type-highlighted {
@apply text-acColor;
}
</style>

View File

@@ -13,8 +13,10 @@ import { GraphQLScalarType } from "graphql"
export default {
props: {
// eslint-disable-next-line vue/require-default-prop
gqlType: null,
// (typeName: string) => void
// eslint-disable-next-line vue/require-default-prop
jumpTypeCallback: Function,
},

View File

@@ -1,5 +1,5 @@
import field from "../Field"
import { shallowMount } from "@vue/test-utils"
import field from "../Field"
const gqlField = {
name: "testField",

View File

@@ -1,7 +1,11 @@
import type from "../Type"
import { shallowMount } from "@vue/test-utils"
import {GraphQLEnumType, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType} from "graphql"
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
} from "graphql"
import type from "../Type"
const gqlType = {
name: "TestType",
@@ -77,11 +81,11 @@ describe("type", () => {
test("prepends 'input' to type name for Input Types", () => {
const testType = new GraphQLInputObjectType({
name: "TestType",
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: testType
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("input")).toEqual(true)
@@ -90,24 +94,26 @@ describe("type", () => {
test("prepends 'interface' to type name for Interface Types", () => {
const testType = new GraphQLInterfaceType({
name: "TestType",
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: testType
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("interface")).toEqual(true)
expect(wrapper.find(".type-title").text().startsWith("interface")).toEqual(
true
)
})
test("prepends 'enum' to type name for Enum Types", () => {
const testType = new GraphQLEnumType({
name: "TestType",
values: {}
values: {},
})
const wrapper = factory({
gqlType: testType
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("enum")).toEqual(true)
@@ -116,34 +122,36 @@ describe("type", () => {
test("'interfaces' computed property returns all the related interfaces", () => {
const testInterfaceA = new GraphQLInterfaceType({
name: "TestInterfaceA",
fields: {}
fields: {},
})
const testInterfaceB = new GraphQLInterfaceType({
name: "TestInterfaceB",
fields: {}
fields: {},
})
const type = new GraphQLObjectType({
name: "TestType",
interfaces: [testInterfaceA, testInterfaceB],
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: type
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual(expect.arrayContaining([testInterfaceA, testInterfaceB]))
expect(wrapper.vm.interfaces).toEqual(
expect.arrayContaining([testInterfaceA, testInterfaceB])
)
})
test("'interfaces' computed property returns an empty array if there are no interfaces", () => {
const type = new GraphQLObjectType({
name: "TestType",
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: type
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual([])
@@ -152,11 +160,11 @@ describe("type", () => {
test("'interfaces' computed property returns an empty array if the type is an enum", () => {
const type = new GraphQLEnumType({
name: "TestType",
values: {}
values: {},
})
const wrapper = factory({
gqlType: type
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual([])
@@ -165,24 +173,24 @@ describe("type", () => {
test("'children' computed property returns all the types implementing an interface", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {}
fields: {},
})
const typeA = new GraphQLObjectType({
name: "TypeA",
interfaces: [testInterface],
fields: {}
fields: {},
})
const typeB = new GraphQLObjectType({
name: "TypeB",
interfaces: [testInterface],
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: testInterface,
gqlTypes: [testInterface, typeA, typeB]
gqlTypes: [testInterface, typeA, typeB],
})
expect(wrapper.vm.children).toEqual(expect.arrayContaining([typeA, typeB]))
@@ -191,22 +199,22 @@ describe("type", () => {
test("'children' computed property returns an empty array if there are no types implementing the interface", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {}
fields: {},
})
const typeA = new GraphQLObjectType({
name: "TypeA",
fields: {}
fields: {},
})
const typeB = new GraphQLObjectType({
name: "TypeB",
fields: {}
fields: {},
})
const wrapper = factory({
gqlType: testInterface,
gqlTypes: [testInterface, typeA, typeB]
gqlTypes: [testInterface, typeA, typeB],
})
expect(wrapper.vm.children).toEqual([])
@@ -215,20 +223,19 @@ describe("type", () => {
test("'children' computed property returns an empty array if the type is an enum", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {}
fields: {},
})
const testType = new GraphQLEnumType({
name: "TestEnum",
values: {}
values: {},
})
const wrapper = factory({
gqlType: testType,
gqlTypes: [testInterface, testType]
gqlTypes: [testInterface, testType],
})
expect(wrapper.vm.children).toEqual([])
})
})

View File

@@ -1,6 +1,6 @@
import typelink from "../TypeLink.vue"
import { shallowMount } from "@vue/test-utils"
import {GraphQLInt} from "graphql"
import { GraphQLInt } from "graphql"
import typelink from "../TypeLink.vue"
const factory = (props) =>
shallowMount(typelink, {
@@ -39,7 +39,7 @@ describe("typelink", () => {
const wrapper = factory({
gqlType: GraphQLInt,
jumpTypeCallback: callback
jumpTypeCallback: callback,
})
await wrapper.trigger("click")

View File

@@ -1,5 +1,5 @@
import GraphqlCard from "../graphql/Card"
import { mount } from "@vue/test-utils"
import GraphqlCard from "../graphql/Card"
const factory = (props) => {
return mount(GraphqlCard, {
@@ -35,20 +35,20 @@ describe("GraphqlCard", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
url,
query,
star: false,
},
})
expect(wrapper).toBeTruthy()
})
test("toggle-star emitted on clicking on star button", async () => {
test("toggle-star emitted on clicking on star button", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
url,
query,
star: true,
},
})
@@ -61,8 +61,8 @@ describe("GraphqlCard", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
url,
query,
star: true,
},
})
@@ -85,12 +85,14 @@ describe("GraphqlCard", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
url,
query,
star: true,
},
})
await wrapper.find("button[data-testid='restore_history_entry']").trigger("click")
await wrapper
.find("button[data-testid='restore_history_entry']")
.trigger("click")
expect(wrapper.emitted("use-entry")).toBeTruthy()
})
@@ -98,12 +100,14 @@ describe("GraphqlCard", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
url,
query,
star: true,
},
})
await wrapper.find("button[data-testid=delete_history_entry]").trigger("click")
await wrapper
.find("button[data-testid=delete_history_entry]")
.trigger("click")
expect(wrapper.emitted("delete-entry")).toBeTruthy()
})
})

View File

@@ -1,284 +0,0 @@
import History from "../"
import { fb } from "~/helpers/fb"
import { shallowMount } from "@vue/test-utils"
const restHistory = [
{
id: "0",
type: "rest",
},
{
id: "1",
type: "rest",
},
{
id: "2",
type: "rest",
},
]
const graphqlHistory = [
{
id: "0",
type: "graphql",
},
{
id: "1",
type: "graphql",
},
{
id: "2",
type: "graphql",
},
]
var localStorageMock = (function () {
var store = {
history: JSON.stringify(restHistory),
graphqlHistory: JSON.stringify(graphqlHistory),
}
return {
getItem: function (key) {
return store[key]
},
setItem: jest.fn(),
clear: jest.fn(),
removeItem: jest.fn(),
}
})()
Object.defineProperty(window, "localStorage", { value: localStorageMock })
jest.mock("~/helpers/fb", () => ({
__esModule: true,
fb: {
currentUser: null,
currentHistory: restHistory,
currentGraphqlHistory: graphqlHistory,
clearHistory: jest.fn(),
clearGraphqlHistory: jest.fn(),
deleteHistory: jest.fn(),
deleteGraphqlHistory: jest.fn(),
},
}))
const factory = (props) => {
return shallowMount(History, {
propsData: props,
stubs: {
"v-popover": {
template: "<div><slot /><slot name='popover' :is-open=true /></div>",
},
HistoryRestCard: {
template: "<div data-testid='rest_card' />",
},
HistoryGraphqlCard: {
template: "<div data-testid='graphql_card' />",
},
AppSection: {
template: "<div><slot /></div>",
},
},
mocks: {
$t: (text) => text,
$toast: {
error() {},
},
},
directives: {
tooltip() {
/* stub */
},
closePopover() {
/* stub */
},
},
})
}
beforeEach(() => {
fb.clearHistory.mockClear()
fb.clearGraphqlHistory.mockClear()
fb.deleteHistory.mockClear()
fb.deleteGraphqlHistory.mockClear()
window.localStorage.setItem.mockClear()
})
describe("Mount History", () => {
test("Mounts rest history without login", async () => {
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts graphql history without login", async () => {
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
})
describe("Clear History", () => {
test("Clear rest history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Clear rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearHistory).toHaveBeenCalledTimes(1)
expect(window.localStorage.setItem).toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Dont confirm Clear rest history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='reject_clear_history']").trigger("click")
expect(fb.clearHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).not.toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Clear graphql history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearGraphqlHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).toHaveBeenCalledWith("graphqlHistory", JSON.stringify([]))
})
test("Clear graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearGraphqlHistory).toHaveBeenCalledTimes(1)
expect(window.localStorage.setItem).toHaveBeenCalledWith("graphqlHistory", JSON.stringify([]))
})
test("Dont confirm Clear graphql history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='reject_clear_history']").trigger("click")
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
"graphqlHistory",
JSON.stringify([])
)
})
})
describe("Use History", () => {
test("use rest history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = restHistory.length - 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("use-entry")
expect(wrapper.emitted("useHistory")).toBeTruthy()
expect(wrapper.emitted("useHistory")[0]).toStrictEqual([restHistory[index]])
})
test("use graphql history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = restHistory.length - 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("use-entry")
expect(wrapper.emitted("useHistory")).toBeTruthy()
expect(wrapper.emitted("useHistory")[0]).toStrictEqual([graphqlHistory[index]])
})
})
describe("Delete History", () => {
test("delete rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("delete-entry")
expect(fb.deleteHistory).toBeCalledWith(restHistory[index])
})
test("delete rest history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("delete-entry")
expect(window.localStorage.setItem).toBeCalledWith(
"history",
JSON.stringify(restHistory.filter((entry) => entry.id != index))
)
})
test("delete graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("delete-entry")
expect(fb.deleteGraphqlHistory).toBeCalledWith(graphqlHistory[index])
})
test("delete graphql history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("delete-entry")
expect(window.localStorage.setItem).toBeCalledWith(
"graphqlHistory",
JSON.stringify(graphqlHistory.filter((entry) => entry.id != index))
)
})
})

View File

@@ -1,5 +1,5 @@
import RestCard from "../rest/Card"
import { mount } from "@vue/test-utils"
import RestCard from "../rest/Card"
const factory = (props) => {
return mount(RestCard, {
@@ -26,7 +26,7 @@ const factory = (props) => {
const url = "https://dummydata.com/get"
const entry = {
type: "rest",
url: url,
url,
method: "GET",
status: 200,
star: false,
@@ -37,7 +37,7 @@ describe("RestCard", () => {
expect(wrapper).toBeTruthy()
})
test("toggle-star emitted on clicking on star button", async () => {
test("toggle-star emitted on clicking on star button", () => {
const wrapper = factory({ entry })
wrapper.find("button[data-testid='star_button']").trigger("click")
@@ -46,13 +46,17 @@ describe("RestCard", () => {
test("use-entry emit on clicking the restore button", async () => {
const wrapper = factory({ entry })
await wrapper.find("button[data-testid='restore_history_entry']").trigger("click")
await wrapper
.find("button[data-testid='restore_history_entry']")
.trigger("click")
expect(wrapper.emitted("use-entry")).toBeTruthy()
})
test("delete-entry emit on clicking the delete button", async () => {
const wrapper = factory({ entry })
await wrapper.find("button[data-testid=delete_history_entry]").trigger("click")
await wrapper
.find("button[data-testid=delete_history_entry]")
.trigger("click")
expect(wrapper.emitted("delete-entry")).toBeTruthy()
})
})

View File

@@ -13,42 +13,46 @@
/>
</li>
<button
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
data-testid="star_button"
class="icon"
:class="{ stared: entry.star }"
@click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
</i>
</button>
<button
data-testid="query_expand"
class="icon"
@click="expand = !expand"
v-tooltip="{
content: expand ? $t('hide_more') : $t('show_more'),
}"
data-testid="query_expand"
class="icon"
@click="expand = !expand"
>
<i class="material-icons">
{{ expand ? "unfold_less" : "unfold_more" }}
</i>
</button>
<v-popover>
<button data-testid="options" class="tooltip-target icon" v-tooltip="$t('options')">
<button
v-tooltip="$t('options')"
data-testid="options"
class="tooltip-target icon"
>
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
data-testid="restore_history_entry"
class="icon"
@click="$emit('use-entry')"
:aria-label="$t('restore')"
v-close-popover
@click="$emit('use-entry')"
>
<i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span>
@@ -56,11 +60,11 @@
</div>
<div>
<button
v-close-popover
data-testid="delete_history_entry"
class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')"
v-close-popover
@click="$emit('delete-entry')"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
@@ -86,11 +90,11 @@
<div v-if="showMore" class="show-on-large-screen">
<li>
<input
v-tooltip="entry.date"
:aria-label="$t('time')"
type="text"
readonly
:value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
@@ -119,24 +123,10 @@
</div>
</template>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
<script>
export default {
props: {
entry: Object,
entry: { type: Object, default: () => {} },
showMore: Boolean,
},
data() {
@@ -153,3 +143,17 @@ export default {
},
}
</script>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<AppSection icon="history" :label="$t('history')" ref="history" no-legend>
<AppSection ref="history" icon="history" :label="$t('history')" no-legend>
<div class="show-on-large-screen">
<input
v-model="filterText"
aria-label="Search"
type="search"
:placeholder="$t('search')"
v-model="filterText"
class="rounded-t-lg"
/>
</div>
@@ -16,9 +16,9 @@
<ul v-for="(entry, index) in filteredHistory" :key="`entry-${index}`">
<HistoryRestCard
v-if="page == 'rest'"
:entry="entry"
:id="index"
:showMore="showMore"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
@@ -26,21 +26,24 @@
<HistoryGraphqlCard
v-if="page == 'graphql'"
:entry="entry"
:showMore="showMore"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
</ul>
</div>
<p :class="{ hidden: filteredHistory.length != 0 || history.length === 0 }" class="info">
<p
:class="{ hidden: filteredHistory.length != 0 || history.length === 0 }"
class="info"
>
{{ $t("nothing_found") }} "{{ filterText }}"
</p>
<p v-if="history.length === 0" class="info">
<i class="material-icons">schedule</i> {{ $t("history_empty") }}
</p>
<div v-if="history.length !== 0" class="rounded-b-lg bg-bgDarkColor">
<div class="row-wrapper" v-if="!isClearingHistory">
<div v-if="!isClearingHistory" class="row-wrapper">
<button
data-testid="clear_history"
class="icon"
@@ -60,22 +63,24 @@
</i>
</button>
</div>
<div class="row-wrapper" v-else>
<p class="info"><i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}</p>
<div v-else class="row-wrapper">
<p class="info">
<i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}
</p>
<div>
<button
v-tooltip="$t('yes')"
data-testid="confirm_clear_history"
class="icon"
@click="clearHistory"
v-tooltip="$t('yes')"
>
<i class="material-icons">done</i>
</button>
<button
v-tooltip="$t('no')"
data-testid="reject_clear_history"
class="icon"
@click="disableHistoryClearing"
v-tooltip="$t('no')"
>
<i class="material-icons">close</i>
</button>
@@ -85,6 +90,89 @@
</AppSection>
</template>
<script>
import {
restHistory$,
graphqlHistory$,
clearRESTHistory,
clearGraphqlHistory,
toggleGraphqlHistoryEntryStar,
toggleRESTHistoryEntryStar,
deleteGraphqlHistoryEntry,
deleteRESTHistoryEntry,
} from "~/newstore/history"
export default {
props: {
page: { type: String, default: null },
},
data() {
return {
filterText: "",
showFilter: false,
isClearingHistory: false,
showMore: false,
}
},
subscriptions() {
return {
history: this.page === "rest" ? restHistory$ : graphqlHistory$,
}
},
computed: {
filteredHistory() {
const filteringHistory = this.history
return filteringHistory.filter((entry) => {
const filterText = this.filterText.toLowerCase()
return Object.keys(entry).some((key) => {
let value = entry[key]
value = typeof value !== "string" ? value.toString() : value
return value.toLowerCase().includes(filterText)
})
})
},
},
methods: {
clearHistory() {
if (this.page === "rest") clearRESTHistory()
else clearGraphqlHistory()
this.isClearingHistory = false
this.$toast.error(this.$t("history_deleted"), {
icon: "delete",
})
},
useHistory(entry) {
this.$emit("useHistory", entry)
},
deleteHistory(entry) {
if (this.page === "rest") deleteRESTHistoryEntry(entry)
else deleteGraphqlHistoryEntry(entry)
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
},
enableHistoryClearing() {
if (!this.history || !this.history.length) return
this.isClearingHistory = true
},
disableHistoryClearing() {
this.isClearingHistory = false
},
toggleCollapse() {
this.showMore = !this.showMore
},
toggleStar(entry) {
if (this.page === "rest") toggleRESTHistoryEntryStar(entry)
else toggleGraphqlHistoryEntryStar(entry)
},
},
}
</script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
@@ -105,106 +193,3 @@ ol {
}
}
</style>
<script>
import { fb } from "~/helpers/fb"
const updateOnLocalStorage = (propertyName, property) =>
window.localStorage.setItem(propertyName, JSON.stringify(property))
export default {
props: {
page: String,
},
data() {
return {
history:
fb.currentUser !== null
? fb.currentHistory
: JSON.parse(
window.localStorage.getItem(this.page == "rest" ? "history" : "graphqlHistory")
) || [],
filterText: "",
showFilter: false,
isClearingHistory: false,
showMore: false,
}
},
computed: {
filteredHistory() {
this.history =
fb.currentUser !== null
? this.page == "rest"
? fb.currentHistory
: fb.currentGraphqlHistory
: JSON.parse(
window.localStorage.getItem(this.page == "rest" ? "history" : "graphqlHistory")
) || []
return this.history.filter((entry) => {
const filterText = this.filterText.toLowerCase()
return Object.keys(entry).some((key) => {
let value = entry[key]
value = typeof value !== "string" ? value.toString() : value
return value.toLowerCase().includes(filterText)
})
})
},
},
methods: {
async clearHistory() {
if (fb.currentUser !== null) {
this.page == "rest" ? await fb.clearHistory() : await fb.clearGraphqlHistory()
}
this.history = []
this.filterText = ""
this.disableHistoryClearing()
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
this.$toast.error(this.$t("history_deleted"), {
icon: "delete",
})
},
useHistory(entry) {
this.$emit("useHistory", entry)
},
async deleteHistory(entry) {
if (this.history.length === 0) {
this.filterText = ""
}
if (fb.currentUser !== null) {
await (this.page == "rest" ? fb.deleteHistory(entry) : fb.deleteGraphqlHistory(entry))
this.history = fb.currentHistory
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
} else {
this.history.splice(this.history.indexOf(entry), 1)
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
}
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
},
addEntry(entry) {
this.history.push(entry)
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
},
enableHistoryClearing() {
if (!this.history || !this.history.length) return
this.isClearingHistory = true
},
disableHistoryClearing() {
this.isClearingHistory = false
},
toggleCollapse() {
this.showMore = !this.showMore
},
async toggleStar(entry) {
if (fb.currentUser !== null) {
this.page == "rest"
? await fb.toggleStar(entry, !entry.star)
: await fb.toggleGraphqlHistoryStar(entry, !entry.star)
}
entry.star = !entry.star
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
},
},
}
</script>

View File

@@ -20,13 +20,13 @@
</li>
<span>
<button
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
data-testid="star_button"
class="icon"
:class="{ stared: entry.star }"
@click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
@@ -48,17 +48,17 @@
</button>
</li> -->
<v-popover>
<button class="tooltip-target icon" v-tooltip="$t('options')">
<button v-tooltip="$t('options')" class="tooltip-target icon">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
v-close-popover
data-testid="restore_history_entry"
class="icon"
@click="$emit('use-entry')"
:aria-label="$t('edit')"
v-close-popover
@click="$emit('use-entry')"
>
<i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span>
@@ -66,11 +66,11 @@
</div>
<div>
<button
v-close-popover
data-testid="delete_history_entry"
class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')"
v-close-popover
@click="$emit('delete-entry')"
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
@@ -95,11 +95,11 @@
<div v-if="showMore" class="show-on-large-screen">
<li>
<input
v-tooltip="entry.date"
:aria-label="$t('time')"
type="text"
readonly
:value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
@@ -128,26 +128,12 @@
</div>
</template>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
<script>
import findStatusGroup from "~/helpers/findStatusGroup"
export default {
props: {
entry: Object,
entry: { type: Object, default: () => {} },
showMore: Boolean,
},
data() {
@@ -167,3 +153,17 @@ export default {
},
}
</script>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -6,9 +6,9 @@
<label for="reqParamList">{{ $t("request_body") }}</label>
<div>
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent('bodyParams', $event)"
v-tooltip.bottom="$t('clear')"
>
<i class="material-icons">clear_all</i>
</button>
@@ -19,7 +19,14 @@
<ul
v-for="(param, index) in bodyParams"
:key="index"
class="border-b border-dashed divide-y md:divide-x border-brdColor divide-dashed divide-brdColor md:divide-y-0"
class="
border-b border-dashed
divide-y
md:divide-x
border-brdColor
divide-dashed divide-brdColor
md:divide-y-0
"
:class="{ 'border-t': index == 0 }"
>
<li>
@@ -27,9 +34,9 @@
:placeholder="`key ${index + 1}`"
:name="`bparam ${index}`"
:value="param.key"
autofocus
@change="updateBodyParams($event, index, `setKeyBodyParams`)"
@keyup.prevent="setRouteQueryState"
autofocus
/>
</li>
<li>
@@ -59,8 +66,6 @@
<div>
<li>
<button
class="icon"
@click="toggleActive(index,param)"
v-tooltip.bottom="{
content: param.hasOwnProperty('active')
? param.active
@@ -68,6 +73,8 @@
: $t('turn_on')
: $t('turn_off'),
}"
class="icon"
@click="toggleActive(index, param)"
>
<i class="material-icons">
{{
@@ -84,7 +91,10 @@
<div v-if="contentType === 'multipart/form-data'">
<li>
<label for="attachment" class="p-0">
<button class="w-full icon" @click="$refs.attachment[index].click()">
<button
class="w-full icon"
@click="$refs.attachment[index].click()"
>
<i class="material-icons">attach_file</i>
</button>
</label>
@@ -92,17 +102,17 @@
ref="attachment"
name="attachment"
type="file"
@change="setRequestAttachment($event, index)"
multiple
@change="setRequestAttachment($event, index)"
/>
</li>
</div>
<div>
<li>
<button
v-tooltip.bottom="$t('delete')"
class="icon"
@click="removeRequestBodyParam(index)"
v-tooltip.bottom="$t('delete')"
>
<i class="material-icons">delete</i>
</button>
@@ -111,7 +121,7 @@
</ul>
<ul>
<li>
<button class="icon" @click="addRequestBodyParam" name="addrequest">
<button class="icon" name="addrequest" @click="addRequestBodyParam">
<i class="material-icons">add</i>
<span>{{ $t("add_new") }}</span>
</button>
@@ -120,26 +130,16 @@
</div>
</template>
<style scoped lang="scss">
.file-chips-container {
@apply flex;
@apply flex-1;
@apply whitespace-nowrap;
@apply overflow-auto;
@apply bg-bgDarkColor;
.file-chips-wrapper {
@apply flex;
@apply w-0;
}
}
</style>
<script>
export default {
props: {
bodyParams: { type: Array, default: () => [] },
},
computed: {
contentType() {
return this.$store.state.request.contentType
},
},
methods: {
clearContent(bodyParams, $event) {
this.$emit("clear-content", bodyParams, $event)
@@ -148,8 +148,13 @@ export default {
this.$emit("set-route-query-state")
},
removeRequestBodyParam(index) {
const paramArr = this.$store.state.request.bodyParams
.filter((item, itemIndex) => itemIndex !== index && (item.hasOwnProperty("active") ? item.active == true : true))
const paramArr = this.$store.state.request.bodyParams.filter(
(item, itemIndex) =>
itemIndex !== index &&
(Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true)
)
this.setRawParams(paramArr)
this.$emit("remove-request-body-param", index)
},
@@ -174,49 +179,70 @@ export default {
fileIndex,
})
},
updateBodyParams(event, index, type){
updateBodyParams(event, index, type) {
this.$store.commit(type, {
index,
value: event.target.value,
})
let paramArr = this.$store.state.request.bodyParams
.filter((item) => (item.hasOwnProperty("active") ? item.active == true : true))
this.setRawParams(paramArr)
const paramArr = this.$store.state.request.bodyParams.filter((item) =>
Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true
)
this.setRawParams(paramArr)
},
toggleActive(index, param){
let paramArr = this.$store.state.request.bodyParams
.filter((item, itemIndex) => {
if(index === itemIndex){
return !param.active
toggleActive(index, param) {
const paramArr = this.$store.state.request.bodyParams.filter(
(item, itemIndex) => {
if (index === itemIndex) {
return !param.active
} else {
return item.hasOwnProperty("active") ? item.active == true : true
return Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true
}
})
}
)
this.setRawParams(paramArr)
this.$store.commit('setActiveBodyParams', {
this.$store.commit("setActiveBodyParams", {
index,
value: param.hasOwnProperty('active') ? !param.active : false,
value: Object.prototype.hasOwnProperty.call(param, "active")
? !param.active
: false,
})
},
setRawParams(filteredParamArr){
setRawParams(filteredParamArr) {
let rawParams = {}
filteredParamArr.forEach(_param=>{
rawParams={
filteredParamArr.forEach((_param) => {
rawParams = {
...rawParams,
[_param.key]:_param.value
[_param.key]: _param.value,
}
})
const rawParamsStr = JSON.stringify(rawParams,null,2)
this.$store.commit("setState", { value: rawParamsStr, attribute: "rawParams" })
}
},
computed: {
contentType() {
return this.$store.state.request.contentType
const rawParamsStr = JSON.stringify(rawParams, null, 2)
this.$store.commit("setState", {
value: rawParamsStr,
attribute: "rawParams",
})
},
},
}
</script>
<style scoped lang="scss">
.file-chips-container {
@apply flex;
@apply flex-1;
@apply whitespace-nowrap;
@apply overflow-auto;
@apply bg-bgDarkColor;
.file-chips-wrapper {
@apply flex;
@apply w-0;
}
}
</style>

View File

@@ -11,10 +11,12 @@
</div>
</div>
<div slot="body" class="flex flex-col">
<label for="requestType">{{ $t("request_type") }}</label>
<label for="requestType">{{ $t("choose_language") }}</label>
<span class="select-wrapper">
<v-popover>
<pre v-if="requestType">{{ codegens.find((x) => x.id === requestType).name }}</pre>
<pre v-if="requestType">{{
codegens.find((x) => x.id === requestType).name
}}</pre>
<input
v-else
id="requestType"
@@ -26,7 +28,11 @@
/>
<template slot="popover">
<div v-for="gen in codegens" :key="gen.id">
<button class="icon" @click="requestType = gen.id" v-close-popover>
<button
v-close-popover
class="icon"
@click="requestType = gen.id"
>
{{ gen.name }}
</button>
</div>
@@ -37,24 +43,31 @@
<label for="generatedCode">{{ $t("generated_code") }}</label>
<div>
<button
class="icon"
@click="copyRequestCode"
ref="copyRequestCode"
v-tooltip="$t('copy_code')"
class="icon"
@click="copyRequestCode"
>
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<textarea
id="generatedCode"
<SmartAceEditor
v-if="requestType"
ref="generatedCode"
name="generatedCode"
rows="8"
v-model="requestCode"
readonly
class="rounded-b-lg"
></textarea>
:value="requestCode"
:lang="codegens.find((x) => x.id === requestType).language"
:options="{
maxLines: '10',
minLines: '10',
fontSize: '16px',
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="rounded-b-lg"
/>
</div>
</SmartModal>
</template>
@@ -65,8 +78,8 @@ import { codegens } from "~/helpers/codegen/codegen"
export default {
props: {
show: Boolean,
requestCode: String,
requestTypeProp: { type: String, default: "" },
requestCode: { type: String, default: null },
requestTypeProp: { type: String, default: "curl" },
},
data() {
return {
@@ -97,9 +110,13 @@ export default {
this.$toast.success(this.$t("copied_to_clipboard"), {
icon: "done",
})
this.$refs.generatedCode.select()
this.$refs.generatedCode.editor.selectAll()
this.$refs.generatedCode.editor.focus()
document.execCommand("copy")
setTimeout(() => (this.$refs.copyRequestCode.innerHTML = this.copyButton), 1000)
setTimeout(
() => (this.$refs.copyRequestCode.innerHTML = this.copyButton),
1000
)
},
},
}

View File

@@ -1,14 +1,14 @@
<template>
<AppSection label="Headers" ref="headers" no-legend>
<AppSection ref="headers" label="Headers" no-legend>
<ul v-if="headers.length !== 0">
<li>
<div class="row-wrapper">
<label for="headerList">{{ $t("header_list") }}</label>
<div>
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent('headers', $event)"
v-tooltip.bottom="$t('clear')"
>
<i class="material-icons">clear_all</i>
</button>
@@ -19,7 +19,14 @@
<ul
v-for="(header, index) in headers"
:key="`${header.value}_${index}`"
class="border-b border-dashed divide-y md:divide-x border-brdColor divide-dashed divide-brdColor md:divide-y-0"
class="
border-b border-dashed
divide-y
md:divide-x
border-brdColor
divide-dashed divide-brdColor
md:divide-y-0
"
:class="{ 'border-t': index == 0 }"
>
<li>
@@ -28,6 +35,7 @@
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
@input="
$store.commit('setKeyHeader', {
index,
@@ -35,7 +43,6 @@
})
"
@keyup.prevent="setRouteQueryState"
autofocus
/>
</li>
<li>
@@ -55,13 +62,6 @@
<div>
<li>
<button
class="icon"
@click="
$store.commit('setActiveHeader', {
index,
value: header.hasOwnProperty('active') ? !header.active : false,
})
"
v-tooltip.bottom="{
content: header.hasOwnProperty('active')
? header.active
@@ -69,6 +69,13 @@
: $t('turn_on')
: $t('turn_off'),
}"
class="icon"
@click="
$store.commit('setActiveHeader', {
index,
value: header.hasOwnProperty('active') ? !header.active : false,
})
"
>
<i class="material-icons">
{{
@@ -84,7 +91,11 @@
</div>
<div>
<li>
<button class="icon" @click="removeRequestHeader(index)" v-tooltip.bottom="$t('delete')">
<button
v-tooltip.bottom="$t('delete')"
class="icon"
@click="removeRequestHeader(index)"
>
<i class="material-icons">delete</i>
</button>
</li>

View File

@@ -11,7 +11,12 @@
</div>
</div>
<div slot="body" class="flex flex-col">
<textarea id="import-curl" autofocus rows="8" :placeholder="$t('enter_curl')"></textarea>
<textarea
id="import-curl"
autofocus
rows="8"
:placeholder="$t('enter_curl')"
></textarea>
</div>
<div slot="footer">
<div class="row-wrapper">

View File

@@ -1,5 +1,5 @@
<template>
<AppSection :label="$t('notes')" ref="sync" no-legend>
<AppSection ref="sync" :label="$t('notes')" no-legend>
<div v-if="fb.currentUser">
<FirebaseInputform />
<FirebaseFeeds />

View File

@@ -1,14 +1,14 @@
<template>
<AppSection label="Parameters" ref="parameters" no-legend>
<AppSection ref="parameters" label="Parameters" no-legend>
<ul v-if="params.length !== 0">
<li>
<div class="row-wrapper">
<label for="paramList">{{ $t("parameter_list") }}</label>
<div>
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent('parameters', $event)"
v-tooltip.bottom="$t('clear')"
>
<i class="material-icons">clear_all</i>
</button>
@@ -19,7 +19,14 @@
<ul
v-for="(param, index) in params"
:key="index"
class="border-b border-dashed divide-y md:divide-x border-brdColor divide-dashed divide-brdColor md:divide-y-0"
class="
border-b border-dashed
divide-y
md:divide-x
border-brdColor
divide-dashed divide-brdColor
md:divide-y-0
"
:class="{ 'border-t': index == 0 }"
>
<li>
@@ -27,13 +34,13 @@
:placeholder="$t('parameter_count', { count: index + 1 })"
:name="'param' + index"
:value="param.key"
autofocus
@change="
$store.commit('setKeyParams', {
index,
value: $event.target.value,
})
"
autofocus
/>
</li>
<li>
@@ -72,13 +79,6 @@
<div>
<li>
<button
class="icon"
@click="
$store.commit('setActiveParams', {
index,
value: param.hasOwnProperty('active') ? !param.active : false,
})
"
v-tooltip.bottom="{
content: param.hasOwnProperty('active')
? param.active
@@ -86,6 +86,13 @@
: $t('turn_on')
: $t('turn_off'),
}"
class="icon"
@click="
$store.commit('setActiveParams', {
index,
value: param.hasOwnProperty('active') ? !param.active : false,
})
"
>
<i class="material-icons">
{{
@@ -101,7 +108,11 @@
</div>
<div>
<li>
<button class="icon" @click="removeRequestParam(index)" v-tooltip.bottom="$t('delete')">
<button
v-tooltip.bottom="$t('delete')"
class="icon"
@click="removeRequestParam(index)"
>
<i class="material-icons">delete</i>
</button>
</li>

View File

@@ -6,24 +6,33 @@
<label for="rawBody">{{ $t("raw_request_body") }}</label>
<div>
<button
class="icon"
ref="prettifyRequest"
@click="prettifyRequestBody"
v-tooltip="$t('prettify_body')"
v-if="rawInput && contentType.endsWith('json')"
ref="prettifyRequest"
v-tooltip="$t('prettify_body')"
class="icon"
@click="prettifyRequestBody"
>
<i class="material-icons">photo_filter</i>
</button>
<label for="payload" class="p-0">
<button class="icon" @click="$refs.payload.click()" v-tooltip="$t('import_json')">
<button
v-tooltip="$t('import_json')"
class="icon"
@click="$refs.payload.click()"
>
<i class="material-icons">post_add</i>
</button>
</label>
<input ref="payload" name="payload" type="file" @change="uploadPayload" />
<input
ref="payload"
name="payload"
type="file"
@change="uploadPayload"
/>
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent('rawParams', $event)"
v-tooltip.bottom="$t('clear')"
>
<i class="material-icons">clear_all</i>
</button>
@@ -53,8 +62,8 @@ import { getEditorLangForMimeType } from "~/helpers/editorutils"
export default {
props: {
rawParams: { type: String, default: "{}" },
contentType: { type: String, default: "" },
rawParams: { type: String, default: null },
contentType: { type: String, default: null },
rawInput: { type: Boolean, default: false },
},
data() {
@@ -102,7 +111,7 @@ export default {
try {
const jsonObj = JSON.parse(this.rawParamsBody)
this.rawParamsBody = JSON.stringify(jsonObj, null, 2)
let oldIcon = this.$refs.prettifyRequest.innerHTML
const oldIcon = this.$refs.prettifyRequest.innerHTML
this.$refs.prettifyRequest.innerHTML = this.doneButton
setTimeout(() => (this.$refs.prettifyRequest.innerHTML = oldIcon), 1000)
} catch (e) {

View File

@@ -1,5 +1,5 @@
<template>
<AppSection id="response" :label="$t('response')" ref="response" no-legend>
<AppSection id="response" ref="response" :label="$t('response')" no-legend>
<HttpResponseMeta :response="response" :active="active" />
<div v-if="response.body && response.body !== $t('loading')">
<LensesResponseBodyRenderer :response="response" />
@@ -12,7 +12,7 @@ export default {
props: {
response: {
type: Object,
default: {},
default: () => {},
},
active: {
type: Boolean,

View File

@@ -2,7 +2,9 @@
<div class="flex flex-col">
<div class="flex items-center justify-between">
<label>{{ $t("response") }}</label>
<label v-if="active"><i class="animate-spin material-icons">refresh</i></label>
<label v-if="active"
><i class="animate-spin material-icons">refresh</i></label
>
<label v-else :class="statusCategory ? statusCategory.className : ''">
<i class="material-icons">fiber_manual_record</i>
</label>
@@ -31,7 +33,7 @@ export default {
props: {
response: {
type: Object,
default: {},
default: () => {},
},
active: {
type: Boolean,

View File

@@ -15,9 +15,9 @@
<label>{{ $t("token_list") }}</label>
<div v-if="tokens.length != 0">
<button
v-tooltip.bottom="$t('clear')"
class="icon"
@click="clearContent('tokens', $event)"
v-tooltip.bottom="$t('clear')"
>
<i class="material-icons">clear_all</i>
</button>
@@ -42,15 +42,19 @@
<div class="row-wrapper">
<li>
<button
v-tooltip.bottom="$t('use_token')"
class="icon"
@click="useOAuthToken(token.value)"
v-tooltip.bottom="$t('use_token')"
>
<i class="material-icons">input</i>
</button>
</li>
<li>
<button class="icon" @click="removeOAuthToken(index)" v-tooltip.bottom="$t('delete')">
<button
v-tooltip.bottom="$t('delete')"
class="icon"
@click="removeOAuthToken(index)"
>
<i class="material-icons">delete</i>
</button>
</li>

View File

@@ -1,7 +1,12 @@
<template>
<div>
<p v-for="(value, key) in headers" :key="key">
<input :value="`${key} → ${value}`" :name="key" class="bg-transparent" readonly />
<input
:value="`${key} → ${value}`"
:name="key"
class="bg-transparent"
readonly
/>
</p>
</div>
</template>
@@ -9,7 +14,7 @@
<script>
export default {
props: {
headers: {},
headers: { type: Object, default: () => {} },
},
}
</script>

View File

@@ -2,8 +2,8 @@
<SmartTabs styles="m-4">
<SmartTab
v-for="(lens, index) in validLenses"
:key="lens.lensName"
:id="lens.lensName"
:key="lens.lensName"
:label="lens.lensName"
:selected="index === 0"
>
@@ -28,7 +28,7 @@ export default {
...getLensRenderers(),
},
props: {
response: {},
response: { type: Object, default: () => {} },
},
computed: {
validLenses() {

View File

@@ -4,13 +4,15 @@
<label for="body">{{ $t("response_body") }}</label>
<div>
<button
v-if="response.body"
ref="ToggleExpandResponse"
v-tooltip="{
content: !expandResponse
? $t('expand_response')
: $t('collapse_response'),
}"
class="icon"
@click="ToggleExpandResponse"
ref="ToggleExpandResponse"
v-if="response.body"
v-tooltip="{
content: !expandResponse ? $t('expand_response') : $t('collapse_response'),
}"
>
<i class="material-icons">
{{ !expandResponse ? "unfold_more" : "unfold_less" }}
@@ -18,31 +20,31 @@
</button>
<button
v-if="response.body"
class="icon"
@click.prevent="togglePreview"
v-tooltip="{
content: previewEnabled ? $t('hide_preview') : $t('preview_html'),
}"
class="icon"
@click.prevent="togglePreview"
>
<i class="material-icons">
{{ !previewEnabled ? "visibility" : "visibility_off" }}
</i>
</button>
<button
v-if="response.body"
ref="downloadResponse"
v-tooltip="$t('download_file')"
class="icon"
@click="downloadResponse"
ref="downloadResponse"
v-if="response.body"
v-tooltip="$t('download_file')"
>
<i class="material-icons">save_alt</i>
</button>
<button
v-if="response.body"
ref="copyResponse"
v-tooltip="$t('copy_response')"
class="icon"
@click="copyResponse"
ref="copyResponse"
v-if="response.body"
v-tooltip="$t('copy_response')"
>
<i class="material-icons">content_copy</i>
</button>
@@ -64,9 +66,9 @@
styles="rounded-b-lg"
/>
<iframe
ref="previewFrame"
:class="{ hidden: !previewEnabled }"
class="covers-response"
ref="previewFrame"
src="about:blank"
></iframe>
</div>
@@ -79,7 +81,7 @@ import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
export default {
mixins: [TextContentRendererMixin],
props: {
response: {},
response: { type: Object, default: () => {} },
},
data() {
return {
@@ -94,7 +96,8 @@ export default {
methods: {
ToggleExpandResponse() {
this.expandResponse = !this.expandResponse
this.responseBodyMaxLines = this.responseBodyMaxLines == Infinity ? 16 : Infinity
this.responseBodyMaxLines =
this.responseBodyMaxLines === Infinity ? 16 : Infinity
},
downloadResponse() {
const dataToWrite = this.responseBodyText
@@ -128,19 +131,30 @@ export default {
aux.select()
document.execCommand("copy")
document.body.removeChild(aux)
setTimeout(() => (this.$refs.copyResponse.innerHTML = this.copyButton), 1000)
setTimeout(
() => (this.$refs.copyResponse.innerHTML = this.copyButton),
1000
)
},
togglePreview() {
this.previewEnabled = !this.previewEnabled
if (this.previewEnabled) {
if (this.$refs.previewFrame.getAttribute("data-previewing-url") === this.url) return
if (
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
this.url
)
return
// Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString(this.responseBodyText, "text/html")
const previewDocument = new DOMParser().parseFromString(
this.responseBodyText,
"text/html"
)
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML =
`<base href="${this.url}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML.
this.$refs.previewFrame.srcdoc = previewDocument.documentElement.outerHTML
this.$refs.previewFrame.srcdoc =
previewDocument.documentElement.outerHTML
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
}
},

Some files were not shown because too many files have changed in this diff Show More