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 test coverage
.nyc_output .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 .grunt
# Bower dependency directory (https://bower.io/) # Bower dependency directory (https://bower.io/)

View File

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

View File

@@ -1,15 +1,18 @@
# Google Analytics # Google Analytics ID
GA_ID=UA-XXXXXXXX-X GA_ID=UA-61422507-4
# Google Tag Manager # Google Tag Manager ID
GTM_ID=GTM-XXXXXXX GTM_ID=GTM-NMKVBMV
# Firebase # Firebase config
API_KEY=api-key API_KEY=AIzaSyCMsFreESs58-hRxTtiqQrIcimh4i1wbsM
AUTH_DOMAIN=project-id.firebaseapp.com AUTH_DOMAIN=postwoman-api.firebaseapp.com
DATABASE_URL=https://project-id.firebaseio.com DATABASE_URL=https://postwoman-api.firebaseio.com
PROJECT_ID=project-id PROJECT_ID=postwoman-api
STORAGE_BUCKET=project-id.appspot.com STORAGE_BUCKET=postwoman-api.appspot.com
MESSAGING_SENDER_ID=sender-id MESSAGING_SENDER_ID=421993993223
APP_ID=app-id APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
MEASUREMENT_ID=G-measurement-id 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 test coverage
.nyc_output .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 .grunt
# Bower dependency directory (https://bower.io/) # Bower dependency directory (https://bower.io/)

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/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 .hoppscotch
.vscode .vscode
package-lock.json 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 # 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) ## [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) [Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.9.7...v1.9.9)

View File

@@ -2,75 +2,131 @@
## Our Pledge ## Our Pledge
In the interest of fostering an open and welcoming environment, we as We as members, contributors, and leaders pledge to make participation in our
contributors and maintainers pledge to making participation in our project and community a harassment-free experience for everyone, regardless of age, body
our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender
size, disability, ethnicity, sex characteristics, gender identity and expression, identity and expression, level of experience, education, socio-economic status,
level of experience, education, socio-economic status, nationality, personal nationality, personal appearance, race, caste, color, religion, or sexual identity
appearance, race, religion, or sexual identity and orientation. and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards ## Our Standards
Examples of behavior that contributes to creating a positive environment Examples of behavior that contributes to a positive environment for our
include: community include:
* Using welcoming and inclusive language - Demonstrating empathy and kindness toward other people
* Being respectful of differing viewpoints and experiences - Being respectful of differing opinions, viewpoints, and experiences
* Gracefully accepting constructive criticism - Giving and gracefully accepting constructive feedback
* Focusing on what is best for the community - Accepting responsibility and apologizing to those affected by our mistakes,
* Showing empathy towards other community members 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 - The use of sexualized language or imagery, and sexual attention or
advances advances of any kind
* Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or electronic - Publishing others' private information, such as a physical or email
address, without explicit permission address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Our Responsibilities ## Enforcement Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable Community leaders are responsible for clarifying and enforcing our standards of
behavior. Maintainers are expected to take appropriate and fair corrective action in acceptable behavior and will take appropriate and fair corrective action in
response to any instances of unacceptable behavior. response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or Community leaders have the right and responsibility to remove, edit, or reject
reject comments, commits, code, wiki edits, issues, and other contributions comments, commits, code, wiki edits, issues, and other contributions that are
that are not aligned with our Code of Conduct, or to ban temporarily or not aligned to this Code of Conduct, and will communicate reasons for moderation
permanently any contributor for other behaviors that they deem inappropriate, decisions when appropriate.
threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies both within project spaces and in public spaces This Code of Conduct applies within all community spaces, and also applies when
when an individual is representing the project or its community. Examples of an individual is officially representing the community in public spaces.
representing a project or community include using an official project e-mail Examples of representing our community include using an official e-mail address,
address, posting via an official social media account, or acting as an appointed 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 representative at an online or offline event.
further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at liyascthomas@gmail.com. All reported to the community leaders responsible for enforcement at
complaints will be reviewed and investigated and will result in a response that [INSERT CONTACT METHOD].
is deemed necessary and appropriate to the circumstances. The project team is All complaints will be reviewed and investigated promptly and fairly.
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 All community leaders are obligated to respect the privacy and security of the
faith may face temporary or permanent repercussions as determined by other reporter of any incident.
members of the project's leadership.
## 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 ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant][homepage],
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 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 [homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
For answers to common questions about this code of conduct, see [mozilla coc]: https://github.com/mozilla/diversity
https://www.contributor-covenant.org/faq [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 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. 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 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 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. 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 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 # Add git as the prebuild target requires it to parse version information
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
@@ -17,9 +17,9 @@ ADD . /app/
COPY . . COPY . .
RUN npm run generate
ENV HOST 0.0.0.0 ENV HOST 0.0.0.0
EXPOSE 3000 EXPOSE 3000
RUN mv .env.example .env
CMD ["npm", "run", "dev"] 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> <b>Hoppscotch - Open source API development ecosystem</b>
</p> </p>
<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>
<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>
<p> <p>
@@ -45,7 +45,7 @@
#### **Contact** #### **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** #### **Support**
@@ -410,6 +410,21 @@ _Notes are only available for signed-in users_
</details> </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).** **To find out more, please check out [Hoppscotch Wiki](https://github.com/hoppscotch/hoppscotch/wiki).**
## **Demo** ## **Demo**
@@ -428,6 +443,7 @@ _Notes are only available for signed-in users_
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) - [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) - [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) - [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org) - [Vue](https://vuejs.org)
- [Nuxt](https://nuxtjs.org) - [Nuxt](https://nuxtjs.org)
@@ -548,6 +564,30 @@ Become a financial contributor and help us sustain our community [[Support](#sup
#### GitHub Sponsors #### GitHub Sponsors
<p align="center"> <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"> <a href="https://github.com/eldadfux" target="_blank" rel="noopener">
<img <img
width="64" width="64"
@@ -597,13 +637,6 @@ Become a financial contributor and help us sustain our community [[Support](#sup
alt="Erica Brescia" alt="Erica Brescia"
/> />
</a> </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"> <a href="https://github.com/mlynch" target="_blank" rel="noopener">
<img <img
width="64" width="64"
@@ -637,6 +670,13 @@ Become a financial contributor and help us sustain our community [[Support](#sup
#### Open Collective #### Open Collective
<p align="center"> <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"> <a href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=github&utm_campaign=hoppscotch-sponsorship" target="_blank" rel="noopener">
<img <img
width="100" width="100"
@@ -646,11 +686,6 @@ Become a financial contributor and help us sustain our community [[Support](#sup
</a> </a>
</p> </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 ### Code Contributors
This project exists thanks to all the people who contribute [[Contribute](CONTRIBUTING.md)]. This project exists thanks to all the people who contribute [[Contribute](CONTRIBUTING.md)].

View File

@@ -1,203 +1,249 @@
/* fallback */ /* fallback */
@font-face { @font-face {
font-family: 'Material Icons'; font-family: "Material Icons";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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 */ /* devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-400-devanagari2.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-400-latin-ext3.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-400-latin4.woff2') format('woff2'); 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; 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 */ /* devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-500-devanagari5.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-500-latin-ext6.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-500-latin7.woff2') format('woff2'); 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; 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 */ /* devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-600-devanagari8.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-600-latin-ext9.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-600-latin10.woff2') format('woff2'); 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; 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 */ /* devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-700-devanagari11.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-700-latin-ext12.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-700-latin13.woff2') format('woff2'); 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; 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 */ /* devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-800-devanagari14.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-800-latin-ext15.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Poppins'; font-family: "Poppins";
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Poppins-800-latin16.woff2') format('woff2'); 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; 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 */ /* cyrillic-ext */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-cyrillic-ext17.woff2') format('woff2'); 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek */ /* greek */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-vietnamese20.woff2') format('woff2'); 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; 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 */ /* latin-ext */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-latin-ext21.woff2') format('woff2'); 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; 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 */ /* latin */
@font-face { @font-face {
font-family: 'Roboto Mono'; font-family: "Roboto Mono";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('~assets/fonts/Roboto_Mono-400-latin22.woff2') format('woff2'); 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; 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 { .material-icons {
font-family: 'Material Icons'; font-family: "Material Icons"; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-size: 24px; font-size: 24px;
@@ -208,6 +254,6 @@
white-space: nowrap; white-space: nowrap;
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-feature-settings: 'liga'; -webkit-font-feature-settings: "liga";
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }

View File

@@ -1,5 +1,6 @@
/* purgecss start ignore */ /* purgecss start ignore */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
/* purgecss end ignore */ /* purgecss end ignore */
@tailwind utilities; @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 // type = ws/sse/socketio
async function validator(type, url) { async function validator(type, url) {
console.time(`validator ${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}`) console.timeEnd(`validator ${url}`)
return res1 || res2 return res1 || res2
} }

View File

@@ -1,218 +1,12 @@
**Table of Contents**
{{#collections}}
{{>folderContents}}
{{/collections}}
{{#collections}} {{#collections}}
# {{name}} {{>folderBody}}
## {{#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}}
{{/collections}} {{/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) { module.exports = function (api) {
if (api.env("test") && !api.caller(isBabelLoader)) { if (api.env("test") && !api.caller(isBabelLoader)) {
return { 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: [ presets: [
[ [
"@babel/preset-env", "@babel/preset-env",

View File

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

View File

@@ -21,13 +21,22 @@
rel="noopener" rel="noopener"
> >
<button class="icon"> <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 <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" 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> </svg>
<span>Firefox</span> <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> <i class="material-icons">done</i>
</span> </span>
</button> </button>
@@ -40,13 +49,22 @@
rel="noopener" rel="noopener"
> >
<button class="icon"> <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 <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" 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> </svg>
<span>Chrome</span> <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> <i class="material-icons">done</i>
</span> </span>
</button> </button>
@@ -67,18 +85,18 @@ export default {
props: { props: {
show: Boolean, show: Boolean,
}, },
watch: {
show() {
this.hasChromeExtInstalled = hasChromeExtensionInstalled()
this.hasFirefoxExtInstalled = hasFirefoxExtensionInstalled()
},
},
data() { data() {
return { return {
hasChromeExtInstalled: hasChromeExtensionInstalled(), hasChromeExtInstalled: hasChromeExtensionInstalled(),
hasFirefoxExtInstalled: hasFirefoxExtensionInstalled(), hasFirefoxExtInstalled: hasFirefoxExtensionInstalled(),
} }
}, },
watch: {
show() {
this.hasChromeExtInstalled = hasChromeExtensionInstalled()
this.hasFirefoxExtInstalled = hasFirefoxExtensionInstalled()
},
},
methods: { methods: {
hideModal() { hideModal() {
this.$emit("hide-modal") this.$emit("hide-modal")

View File

@@ -1,11 +1,18 @@
<template> <template>
<footer class="footer"> <footer class="footer">
<div class="row-wrapper"> <div class="row-wrapper">
<span class="flex flex-col font-mono md:flex-row" style="align-items: start"> <span
<a class="footer-link" href="https://www.netlify.com" target="_blank" rel="noopener"> 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 Powered by Netlify
</a> </a>
<span class="footer-link"> Sponsored by </span>
<span> <span>
<a <a
class="footer-link" class="footer-link"
@@ -15,9 +22,6 @@
> >
OSS Capital OSS Capital
</a> </a>
</span>
<span class="footer-link"> & </span>
<span>
<a <a
class="footer-link" class="footer-link"
href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=website&utm_campaign=hoppscotch-sponsorship" href="https://paw.cloud/?utm_source=hoppscotch&utm_medium=website&utm_campaign=hoppscotch-sponsorship"
@@ -26,6 +30,14 @@
> >
Paw Paw
</a> </a>
<a
class="footer-link"
href="https://simplescraper.io/?utm_source=hs"
target="_blank"
rel="noopener"
>
Simplescraper
</a>
</span> </span>
<iframe <iframe
src="https://ghbtns.com/github-btn.html?user=hoppscotch&type=sponsor" src="https://ghbtns.com/github-btn.html?user=hoppscotch&type=sponsor"
@@ -38,20 +50,23 @@
loading="lazy" loading="lazy"
></iframe> ></iframe>
</span> </span>
<span class="flex flex-col font-mono md:flex-row" style="align-items: start"> <span
<a href="mailto:liyascthomas@gmail.com" target="_blank" rel="noopener"> class="flex flex-col font-mono md:flex-row"
<button class="icon" v-tooltip="$t('contact_us')"> 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> <i class="material-icons">email</i>
</button> </button>
</a> </a>
<v-popover> <v-popover>
<button class="icon" v-tooltip="$t('choose_language')"> <button v-tooltip="$t('choose_language')" class="icon">
<i class="material-icons">translate</i> <i class="material-icons">translate</i>
</button> </button>
<template slot="popover"> <template slot="popover">
<div v-for="locale in availableLocales" :key="locale.code"> <div v-for="locale in availableLocales" :key="locale.code">
<nuxt-link :to="switchLocalePath(locale.code)"> <nuxt-link :to="switchLocalePath(locale.code)">
<button class="icon" v-close-popover> <button v-close-popover class="icon">
{{ locale.name }} {{ locale.name }}
</button> </button>
</nuxt-link> </nuxt-link>
@@ -63,20 +78,6 @@
</footer> </footer>
</template> </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> <script>
export default { export default {
computed: { computed: {
@@ -86,3 +87,18 @@ export default {
}, },
} }
</script> </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" href="https://appwrite.io/?utm_source=hoppscotch&utm_medium=banner&utm_campaign=hello"
target="_blank" target="_blank"
rel="noopener" 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 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> </a>
<button <button
class="icon"
id="installPWA" id="installPWA"
@click.prevent="showInstallPrompt()"
v-tooltip="$t('install_pwa')" v-tooltip="$t('install_pwa')"
class="icon"
@click.prevent="showInstallPrompt()"
> >
<i class="material-icons">offline_bolt</i> <i class="material-icons">offline_bolt</i>
</button> </button>
@@ -41,8 +56,13 @@
aria-label="GitHub" aria-label="GitHub"
rel="noopener" rel="noopener"
> >
<button class="icon" aria-label="GitHub" v-tooltip="'GitHub'"> <button v-tooltip="'GitHub'" class="icon" aria-label="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons"> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path <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" 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> </button>
</a> </a>
<v-popover v-if="fb.currentUser === null"> <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> <i class="material-icons">login</i>
</button> </button>
<template slot="popover"> <template slot="popover">
@@ -59,12 +79,13 @@
</v-popover> </v-popover>
<v-popover v-else> <v-popover v-else>
<button <button
class="icon"
v-tooltip=" v-tooltip="
(fb.currentUser.displayName || '<label><i>Name not found</i></label>') + (fb.currentUser.displayName ||
'<label><i>Name not found</i></label>') +
'<br>' + '<br>' +
(fb.currentUser.email || '<label><i>Email not found</i></label>') (fb.currentUser.email || '<label><i>Email not found</i></label>')
" "
class="icon"
aria-label="Account" aria-label="Account"
> >
<img <img
@@ -77,7 +98,7 @@
</button> </button>
<template slot="popover"> <template slot="popover">
<div> <div>
<nuxt-link :to="localePath('settings')" v-close-popover> <nuxt-link v-close-popover :to="localePath('settings')">
<button class="icon"> <button class="icon">
<i class="material-icons">settings</i> <i class="material-icons">settings</i>
<span> <span>
@@ -92,26 +113,26 @@
</template> </template>
</v-popover> </v-popover>
<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> <i class="material-icons">drag_indicator</i>
</button> </button>
<template slot="popover"> <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> <i class="material-icons">extension</i>
<span>{{ $t("extensions") }}</span> <span>{{ $t("extensions") }}</span>
</button> </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> <i class="material-icons">keyboard</i>
<span>{{ $t("shortcuts") }}</span> <span>{{ $t("shortcuts") }}</span>
</button> </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> <i class="material-icons">favorite</i>
<span>{{ $t("support_us") }}</span> <span>{{ $t("support_us") }}</span>
</button> </button>
<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 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"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path <path
@@ -122,10 +143,10 @@
</button> </button>
<button <button
v-if="navigatorShare" v-if="navigatorShare"
class="icon"
@click="nativeShare"
v-close-popover v-close-popover
v-tooltip="$t('more')" v-tooltip="$t('more')"
class="icon"
@click="nativeShare"
> >
<i class="material-icons">share</i> <i class="material-icons">share</i>
<span>Share</span> <span>Share</span>
@@ -134,12 +155,149 @@
</v-popover> </v-popover>
</span> </span>
</div> </div>
<AppExtensions :show="showExtensions" @hide-modal="showExtensions = false" /> <AppExtensions
:show="showExtensions"
@hide-modal="showExtensions = false"
/>
<AppShortcuts :show="showShortcuts" @hide-modal="showShortcuts = false" /> <AppShortcuts :show="showShortcuts" @hide-modal="showShortcuts = false" />
<AppSupport :show="showSupport" @hide-modal="showSupport = false" /> <AppSupport :show="showSupport" @hide-modal="showSupport = false" />
</header> </header>
</template> </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"> <style scoped lang="scss">
$responsiveWidth: 768px; $responsiveWidth: 768px;
@@ -182,138 +340,3 @@ $responsiveWidth: 768px;
} }
} }
</style> </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: { props: {
color: { color: {
type: String, type: String,
default: "",
}, },
}, },
} }

View File

@@ -6,12 +6,51 @@
{{ isCollapsed(label) ? "expand_more" : "expand_less" }} {{ isCollapsed(label) ? "expand_more" : "expand_less" }}
</i> </i>
</legend> </legend>
<div class="collapsible" :class="{ hidden: isCollapsed(label.toLowerCase()) }"> <div
<slot /> class="collapsible"
:class="{ hidden: isCollapsed(label.toLowerCase()) }"
>
<slot></slot>
</div> </div>
</fieldset> </fieldset>
</template> </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"> <style scoped lang="scss">
fieldset { fieldset {
@apply my-4; @apply my-4;
@@ -33,35 +72,3 @@ fieldset {
} }
} }
</style> </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> </SmartModal>
</template> </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> <script>
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey } from "~/helpers/platformutils"
@@ -100,3 +88,15 @@ export default {
}, },
} }
</script> </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. seems to mess up the nuxt-link active class.
--> -->
<nuxt-link <nuxt-link
v-tooltip.right="$t('home')"
:to="localePath('index')" :to="localePath('index')"
:class="linkActive('/')" :class="linkActive('/')"
v-tooltip.right="$t('home')"
:aria-label="$t('home')" :aria-label="$t('home')"
> >
<AppLogo alt class="material-icons" style="height: 24px" /> <AppLogo alt class="material-icons" style="height: 24px" />
</nuxt-link> </nuxt-link>
<nuxt-link <nuxt-link
v-tooltip.right="$t('realtime')"
:to="localePath('realtime')" :to="localePath('realtime')"
:class="linkActive('/realtime')" :class="linkActive('/realtime')"
v-tooltip.right="$t('realtime')"
> >
<i class="material-icons">language</i> <i class="material-icons">language</i>
</nuxt-link> </nuxt-link>
<nuxt-link <nuxt-link
v-tooltip.right="$t('graphql')"
:to="localePath('graphql')" :to="localePath('graphql')"
:class="linkActive('/graphql')" :class="linkActive('/graphql')"
v-tooltip.right="$t('graphql')"
:aria-label="$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="M4.08 22.864l-1.1-.636L15.248.98l1.1.636z" />
<path d="M2.727 20.53h24.538v1.272H2.727z" /> <path d="M2.727 20.53h24.538v1.272H2.727z" />
<path <path
@@ -43,80 +48,111 @@
</svg> </svg>
</nuxt-link> </nuxt-link>
<nuxt-link <nuxt-link
v-tooltip.right="$t('documentation')"
:to="localePath('doc')" :to="localePath('doc')"
:class="linkActive('/doc')" :class="linkActive('/doc')"
v-tooltip.right="$t('documentation')"
:aria-label="$t('documentation')" :aria-label="$t('documentation')"
> >
<i class="material-icons">topic</i> <i class="material-icons">topic</i>
</nuxt-link> </nuxt-link>
<nuxt-link <nuxt-link
v-tooltip.right="$t('settings')"
:to="localePath('settings')" :to="localePath('settings')"
:class="linkActive('/settings')" :class="linkActive('/settings')"
v-tooltip.right="$t('settings')"
:aria-label="$t('settings')" :aria-label="$t('settings')"
> >
<i class="material-icons">settings</i> <i class="material-icons">settings</i>
</nuxt-link> </nuxt-link>
</nav> </nav>
<nav v-if="$route.path == '/'" class="secondary-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> <i class="material-icons">cloud_upload</i>
</a> </a>
<a href="#options" v-tooltip.right="$t('options')"> <a v-tooltip.right="$t('options')" href="#options">
<i class="material-icons">toc</i> <i class="material-icons">toc</i>
</a> </a>
<a href="#response" v-tooltip.right="$t('response')"> <a v-tooltip.right="$t('response')" href="#response">
<i class="material-icons">cloud_download</i> <i class="material-icons">cloud_download</i>
</a> </a>
</nav> </nav>
<nav v-else-if="$route.path.includes('/realtime')" class="secondary-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> <i class="material-icons">cloud_upload</i>
</a> </a>
<a href="#response" v-tooltip.right="$t('communication')"> <a v-tooltip.right="$t('communication')" href="#response">
<i class="material-icons">cloud_download</i> <i class="material-icons">cloud_download</i>
</a> </a>
</nav> </nav>
<nav v-else-if="$route.path.includes('/graphql')" class="secondary-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> <i class="material-icons">cloud</i>
</a> </a>
<a href="#schema" v-tooltip.right="$t('schema')"> <a v-tooltip.right="$t('schema')" href="#schema">
<i class="material-icons">assignment_returned</i> <i class="material-icons">assignment_returned</i>
</a> </a>
<a href="#query" v-tooltip.right="$t('query')"> <a v-tooltip.right="$t('query')" href="#query">
<i class="material-icons">cloud_upload</i> <i class="material-icons">cloud_upload</i>
</a> </a>
<a href="#response" v-tooltip.right="$t('response')"> <a v-tooltip.right="$t('response')" href="#response">
<i class="material-icons">cloud_download</i> <i class="material-icons">cloud_download</i>
</a> </a>
</nav> </nav>
<nav v-else-if="$route.path.includes('/doc')" class="secondary-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> <i class="material-icons">folder</i>
</a> </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> <i class="material-icons">insert_drive_file</i>
</a> </a>
</nav> </nav>
<nav v-else-if="$route.path.includes('/settings')" class="secondary-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> <i class="material-icons">person</i>
</a> </a>
<a href="#theme" v-tooltip.right="$t('theme')"> <a v-tooltip.right="$t('theme')" href="#theme">
<i class="material-icons">brush</i> <i class="material-icons">brush</i>
</a> </a>
<a href="#extensions" v-tooltip.right="$t('extensions')"> <a v-tooltip.right="$t('extensions')" href="#extensions">
<i class="material-icons">extension</i> <i class="material-icons">extension</i>
</a> </a>
<a href="#proxy" v-tooltip.right="$t('proxy')"> <a v-tooltip.right="$t('proxy')" href="#proxy">
<i class="material-icons">public</i> <i class="material-icons">public</i>
</a> </a>
</nav> </nav>
</aside> </aside>
</template> </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"> <style scoped lang="scss">
$responsiveWidth: 768px; $responsiveWidth: 768px;
@@ -269,39 +305,3 @@ nav.secondary-nav {
} }
} }
</style> </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"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label> <label for="selectLabel">{{ $t("label") }}</label>
<input <input
type="text"
id="selectLabel" id="selectLabel"
v-model="name" v-model="name"
type="text"
:placeholder="$t('my_new_collection')" :placeholder="$t('my_new_collection')"
@keyup.enter="addNewCollection" @keyup.enter="addNewCollection"
/> />
@@ -37,47 +37,23 @@
</template> </template>
<script> <script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
}, },
data() { data() {
return { return {
name: undefined, name: null,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
} }
}, },
methods: { methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
addNewCollection() { addNewCollection() {
if (!this.$data.name) { this.$emit("submit", this.name)
this.$toast.info(this.$t("invalid_collection_name")) this.hideModal()
return
}
this.$store.commit("postwoman/addNewCollection", {
name: this.$data.name,
flag: "rest",
})
this.$emit("hide-modal")
this.syncCollections()
}, },
hideModal() { hideModal() {
this.name = null
this.$emit("hide-modal") this.$emit("hide-modal")
this.$data.name = undefined
}, },
}, },
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<SmartModal v-if="show" @close="show = false"> <SmartModal v-if="show" @close="$emit('hide-modal')">
<div slot="header"> <div slot="header">
<div class="row-wrapper"> <div class="row-wrapper">
<h3 class="title">{{ $t("new_folder") }}</h3> <h3 class="title">{{ $t("new_folder") }}</h3>
@@ -13,9 +13,9 @@
<div slot="body" class="flex flex-col"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label> <label for="selectLabel">{{ $t("label") }}</label>
<input <input
type="text"
id="selectLabel" id="selectLabel"
v-model="name" v-model="name"
type="text"
:placeholder="$t('my_new_folder')" :placeholder="$t('my_new_folder')"
@keyup.enter="addFolder" @keyup.enter="addFolder"
/> />
@@ -40,13 +40,13 @@
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
folder: Object, folder: { type: Object, default: () => {} },
folderPath: String, folderPath: { type: String, default: null },
collectionIndex: Number, collectionIndex: { type: Number, default: null },
}, },
data() { data() {
return { return {
name: undefined, name: null,
} }
}, },
methods: { methods: {
@@ -56,8 +56,10 @@ export default {
folder: this.folder, folder: this.folder,
path: this.folderPath || `${this.collectionIndex}`, path: this.folderPath || `${this.collectionIndex}`,
}) })
this.hideModal()
}, },
hideModal() { hideModal() {
this.name = null
this.$emit("hide-modal") 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"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label> <label for="selectLabel">{{ $t("label") }}</label>
<input <input
type="text"
id="selectLabel" id="selectLabel"
v-model="name" v-model="name"
:placeholder="editingCollection.name" type="text"
:placeholder="placeholderCollName"
@keyup.enter="saveCollection" @keyup.enter="saveCollection"
/> />
</div> </div>
@@ -37,52 +37,23 @@
</template> </template>
<script> <script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
editingCollection: Object, placeholderCollName: { type: String, default: null },
editingCollectionIndex: Number,
}, },
data() { data() {
return { return {
name: undefined, name: null,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
} }
}, },
methods: { methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
saveCollection() { saveCollection() {
if (!this.$data.name) { this.$emit("submit", this.name)
this.$toast.info(this.$t("invalid_collection_name")) this.hideModal()
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()
}, },
hideModal() { hideModal() {
this.name = null
this.$emit("hide-modal") this.$emit("hide-modal")
}, },
}, },

View File

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

View File

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

View File

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

View File

@@ -12,46 +12,21 @@
</div> </div>
<div slot="body" class="flex flex-col"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("token_req_name") }}</label> <label for="selectLabel">{{ $t("token_req_name") }}</label>
<input type="text" id="selectLabel" v-model="requestData.name" @keyup.enter="saveRequestAs" /> <input
<ul> id="selectLabel"
<li> v-model="requestData.name"
<label for="selectCollection">{{ $t("collection") }}</label> type="text"
<span class="select-wrapper"> @keyup.enter="saveRequestAs"
<select type="text" id="selectCollection" v-model="requestData.collectionIndex"> />
<option :key="undefined" :value="undefined" hidden disabled selected> <label for="selectLabel">Select location</label>
{{ $t("select_collection") }} <!-- <input readonly :value="path" /> -->
</option> <Collections
<option :picked="picked"
v-for="(collection, index) in $store.state.postwoman.collections" :save-request="true"
:key="index" @select="onSelect"
:value="index" @update-collection="collectionsType.type = $event"
> @update-coll-type="onUpdateCollType"
{{ collection.name }}
</option>
</select>
</span>
</li>
</ul>
<label>{{ $t("folder") }}</label>
<SmartAutoComplete
: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">
<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>
<div slot="footer"> <div slot="footer">
<div class="row-wrapper"> <div class="row-wrapper">
@@ -72,21 +47,28 @@
<script> <script>
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings" import { getSettingSubject } from "~/newstore/settings"
import * as teamUtils from "~/helpers/teams/utils"
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
editingRequest: Object, editingRequest: { type: Object, default: () => {} },
}, },
data() { data() {
return { return {
defaultRequestName: "Untitled Request", defaultRequestName: "Untitled Request",
path: "Path will appear here",
requestData: { requestData: {
name: undefined, name: undefined,
collectionIndex: undefined, collectionIndex: undefined,
folderName: undefined, folderName: undefined,
requestIndex: undefined, requestIndex: undefined,
}, },
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
picked: null,
} }
}, },
subscriptions() { subscriptions() {
@@ -94,20 +76,6 @@ export default {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"), 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: { computed: {
folders() { folders() {
const collections = this.$store.state.postwoman.collections const collections = this.$store.state.postwoman.collections
@@ -130,7 +98,8 @@ export default {
return [] return []
} }
const userSelectedAnyFolder = folderName !== undefined && folderName !== "" const userSelectedAnyFolder =
folderName !== undefined && folderName !== ""
if (userSelectedAnyFolder) { if (userSelectedAnyFolder) {
const collection = collections[collectionIndex] 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: { methods: {
onUpdateCollType(newCollType) {
this.collectionsType = newCollType
},
onSelect({ picked }) {
this.picked = picked
},
syncCollections() { syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) { if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections( fb.writeCollections(
@@ -158,8 +147,7 @@ export default {
} }
}, },
saveRequestAs() { saveRequestAs() {
const userDidntSpecifyCollection = this.$data.requestData.collectionIndex === undefined if (this.picked == null) {
if (userDidntSpecifyCollection) {
this.$toast.error(this.$t("select_collection"), { this.$toast.error(this.$t("select_collection"), {
icon: "error", icon: "error",
}) })
@@ -178,16 +166,61 @@ export default {
collection: this.$data.requestData.collectionIndex, collection: this.$data.requestData.collectionIndex,
} }
this.$store.commit("postwoman/saveRequestAs", { if (this.picked.pickedType === "my-request") {
request: requestUpdated, this.$store.commit("postwoman/saveRequestAs", {
collectionIndex: this.$data.requestData.collectionIndex, request: requestUpdated,
folderName: this.$data.requestData.folderName, collectionIndex: this.picked.collectionIndex,
requestIndex: this.$data.requestData.requestIndex, folderName: this.picked.folderName,
flag: "rest", 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.hideModal()
this.syncCollections()
}, },
hideModal() { hideModal() {
this.$emit("hide-modal") this.$emit("hide-modal")
@@ -195,12 +228,12 @@ export default {
}, },
} }
function getFolderNames(folders, namesList) { function getFolderNames(folders, namesList, folderName = "") {
if (folders.length) { if (folders.length) {
folders.forEach((folder) => { folders.forEach((folder) => {
namesList.push(folder.name) namesList.push(folderName + folder.name)
if (folder.folders && folder.folders.length) { 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"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label> <label for="selectLabel">{{ $t("label") }}</label>
<input <input
type="text"
id="selectLabel" id="selectLabel"
v-model="name" v-model="name"
type="text"
:placeholder="$t('my_new_collection')" :placeholder="$t('my_new_collection')"
@keyup.enter="addNewCollection" @keyup.enter="addNewCollection"
/> />
@@ -45,7 +45,7 @@ export default {
}, },
data() { data() {
return { return {
name: undefined, name: null,
} }
}, },
methods: { methods: {
@@ -53,7 +53,9 @@ export default {
if (fb.currentUser !== null && fb.currentSettings[0]) { if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) { if (fb.currentSettings[0].value) {
fb.writeCollections( fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)), JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql" "collectionsGraphql"
) )
} }
@@ -68,12 +70,12 @@ export default {
name: this.$data.name, name: this.$data.name,
flag: "graphql", flag: "graphql",
}) })
this.$emit("hide-modal")
this.syncCollections() this.syncCollections()
this.hideModal()
}, },
hideModal() { hideModal() {
this.name = null
this.$emit("hide-modal") this.$emit("hide-modal")
this.$data.name = undefined
}, },
}, },
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,17 +12,33 @@
</div> </div>
<div slot="body" class="flex flex-col"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("token_req_name") }}</label> <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> <ul>
<li> <li>
<label for="selectCollection">{{ $t("collection") }}</label> <label for="selectCollection">{{ $t("collection") }}</label>
<span class="select-wrapper"> <span class="select-wrapper">
<select type="text" id="selectCollection" v-model="requestData.collectionIndex"> <select
<option :key="undefined" :value="undefined" hidden disabled selected> id="selectCollection"
v-model="requestData.collectionIndex"
type="text"
>
<option
:key="undefined"
:value="undefined"
hidden
disabled
selected
>
{{ $t("select_collection") }} {{ $t("select_collection") }}
</option> </option>
<option <option
v-for="(collection, index) in $store.state.postwoman.collectionsGraphql" v-for="(collection, index) in $store.state.postwoman
.collectionsGraphql"
:key="index" :key="index"
:value="index" :value="index"
> >
@@ -34,18 +50,26 @@
</ul> </ul>
<label>{{ $t("folder") }}</label> <label>{{ $t("folder") }}</label>
<SmartAutoComplete <SmartAutoComplete
v-model="requestData.folderName"
:placeholder="$t('search')" :placeholder="$t('search')"
:source="folders" :source="folders"
:spellcheck="false" :spellcheck="false"
v-model="requestData.folderName"
/> />
<ul> <ul>
<li> <li>
<label for="selectRequest">{{ $t("request") }}</label> <label for="selectRequest">{{ $t("request") }}</label>
<span class="select-wrapper"> <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 :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 }} {{ folder.name }}
</option> </option>
</select> </select>
@@ -75,7 +99,7 @@ import { fb } from "~/helpers/fb"
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
editingRequest: Object, editingRequest: { type: Object, default: () => {} },
}, },
data() { data() {
return { 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: { computed: {
folders() { folders() {
const collections = this.$store.state.postwoman.collectionsGraphql const collections = this.$store.state.postwoman.collectionsGraphql
@@ -124,7 +134,8 @@ export default {
return [] return []
} }
const userSelectedAnyFolder = folderName !== undefined && folderName !== "" const userSelectedAnyFolder =
folderName !== undefined && folderName !== ""
if (userSelectedAnyFolder) { if (userSelectedAnyFolder) {
const collection = collections[collectionIndex] 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: { methods: {
syncCollections() { syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) { if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) { if (fb.currentSettings[0].value) {
fb.writeCollections( fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collectionsGraphql)), JSON.parse(
JSON.stringify(this.$store.state.postwoman.collectionsGraphql)
),
"collectionsGraphql" "collectionsGraphql"
) )
} }
} }
}, },
saveRequestAs() { saveRequestAs() {
const userDidntSpecifyCollection = this.$data.requestData.collectionIndex === undefined const userDidntSpecifyCollection =
this.$data.requestData.collectionIndex === undefined
if (userDidntSpecifyCollection) { if (userDidntSpecifyCollection) {
this.$toast.error(this.$t("select_collection"), { this.$toast.error(this.$t("select_collection"), {
icon: "error", icon: "error",

View File

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

View File

@@ -1,20 +1,32 @@
<template> <template>
<AppSection :label="$t('collections')" ref="collections" no-legend> <AppSection ref="collections" :label="$t('collections')" no-legend>
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<input <input
v-if="!saveRequest"
v-model="filterText"
aria-label="Search" aria-label="Search"
type="search" type="search"
:placeholder="$t('search')" :placeholder="$t('search')"
v-model="filterText"
class="rounded-t-lg" class="rounded-t-lg"
/> />
</div> </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 <CollectionsEdit
:show="showModalEdit" :show="showModalEdit"
:editing-collection="editingCollection" :editing-coll-name="editingCollection ? editingCollection.name : ''"
:editing-collection-index="editingCollectionIndex"
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
@submit="updateEditingCollection"
/> />
<CollectionsAddFolder <CollectionsAddFolder
:show="showModalAddFolder" :show="showModalAddFolder"
@@ -25,73 +37,120 @@
/> />
<CollectionsEditFolder <CollectionsEditFolder
:show="showModalEditFolder" :show="showModalEditFolder"
:collection-index="editingCollectionIndex" @submit="updateEditingFolder"
:folder="editingFolder"
:folder-index="editingFolderIndex"
@hide-modal="displayModalEditFolder(false)" @hide-modal="displayModalEditFolder(false)"
/> />
<CollectionsEditRequest <CollectionsEditRequest
:show="showModalEditRequest" :show="showModalEditRequest"
:collection-index="editingCollectionIndex" :placeholder-req-name="editingRequest ? editingRequest.name : ''"
:folder-index="editingFolderIndex" @submit="updateEditingRequest"
:folder-name="editingFolderName"
:request="editingRequest"
:request-index="editingRequestIndex"
@hide-modal="displayModalEditRequest(false)" @hide-modal="displayModalEditRequest(false)"
/> />
<CollectionsImportExport <CollectionsImportExport
:show="showModalImportExport" :show="showModalImportExport"
:collections-type="collectionsType"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
@update-team-collections="updateTeamCollections"
/> />
<div class="border-b row-wrapper border-brdColor"> <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> <i class="material-icons">add</i>
<span>{{ $t("new") }}</span> <span>{{ $t("new") }}</span>
</button> </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") }} {{ $t("import_export") }}
</button> </button>
</div> </div>
<p v-if="collections.length === 0" class="info"> <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> </p>
<div class="virtual-list"> <div class="virtual-list">
<ul class="flex-col"> <ul class="flex-col">
<li v-for="(collection, index) in filteredCollections" :key="collection.name"> <li
<CollectionsCollection v-for="(collection, index) in filteredCollections"
:key="collection.name"
>
<component
:is="
collectionsType.type == 'my-collections'
? 'CollectionsMyCollection'
: 'CollectionsTeamsCollection'
"
:name="collection.name" :name="collection.name"
:collection-index="index" :collection-index="index"
:collection="collection" :collection="collection"
:doc="doc" :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)" @edit-collection="editCollection(collection, index)"
@add-folder="addFolder($event)" @add-folder="addFolder($event)"
@edit-folder="editFolder($event)" @edit-folder="editFolder($event)"
@edit-request="editRequest($event)" @edit-request="editRequest($event)"
@update-team-collections="updateTeamCollections"
@select-collection="$emit('use-collection', collection)" @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> </li>
</ul> </ul>
</div> </div>
<p v-if="filterText && filteredCollections.length === 0" class="info"> <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> </p>
</AppSection> </AppSection>
</template> </template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>
<script> <script>
import gql from "graphql-tag"
import cloneDeep from "lodash/cloneDeep"
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings" import { getSettingSubject } from "~/newstore/settings"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import * as teamUtils from "~/helpers/teams/utils"
export default { export default {
props: { props: {
doc: Boolean, doc: Boolean,
selected: { type: Array, default: () => [] },
saveRequest: Boolean,
picked: { type: Object, default: () => {} },
}, },
data() { data() {
return { return {
@@ -110,6 +169,12 @@ export default {
editingRequest: undefined, editingRequest: undefined,
editingRequestIndex: undefined, editingRequestIndex: undefined,
filterText: "", filterText: "",
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
teamCollectionAdapter: new TeamCollectionAdapter(null),
teamCollectionsNew: [],
} }
}, },
subscriptions() { subscriptions() {
@@ -118,29 +183,51 @@ export default {
} }
}, },
computed: { computed: {
showTeamCollections() {
if (fb.currentUser == null) {
return false
}
return true
},
collections() { collections() {
return fb.currentUser !== null return fb.currentUser !== null
? fb.currentCollections ? fb.currentCollections
: this.$store.state.postwoman.collections : this.$store.state.postwoman.collections
}, },
filteredCollections() { filteredCollections() {
const collections = let collections = null
fb.currentUser !== null ? fb.currentCollections : this.$store.state.postwoman.collections 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 filterText = this.filterText.toLowerCase()
const filteredCollections = [] const filteredCollections = []
for (let collection of collections) { for (const collection of collections) {
const filteredRequests = [] const filteredRequests = []
const filteredFolders = [] const filteredFolders = []
for (let request of collection.requests) { for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText)) filteredRequests.push(request) 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 = [] const filteredFolderRequests = []
for (let request of folder.requests) { for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText)) if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request) 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) const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders filteredCollection.folders = filteredFolders
@@ -162,16 +252,202 @@ export default {
return filteredCollections 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) { this._keyListener = function (e) {
if (e.key === "Escape") { if (e.key === "Escape") {
e.preventDefault() 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)) document.addEventListener("keydown", this._keyListener.bind(this))
this.$subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
this.teamCollectionsNew = cloneDeep(colls)
})
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
}, },
methods: { 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) { displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay this.showModalAdd = shouldDisplay
}, },
@@ -204,16 +480,56 @@ export default {
this.displayModalEdit(true) this.displayModalEdit(true)
this.syncCollections() this.syncCollections()
}, },
onAddFolder({ name, path }) { onAddFolder({ name, folder, path }) {
const flag = "rest" const flag = "rest"
this.$store.commit("postwoman/addFolder", { if (this.collectionsType.type === "my-collections") {
name, this.$store.commit("postwoman/addFolder", {
path, name,
flag, 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.displayModalAddFolder(false)
this.syncCollections()
}, },
addFolder(payload) { addFolder(payload) {
const { folder, path } = payload const { folder, path } = payload
@@ -226,16 +542,24 @@ export default {
this.$data.editingCollectionIndex = collectionIndex this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolder = folder this.$data.editingFolder = folder
this.$data.editingFolderIndex = folderIndex this.$data.editingFolderIndex = folderIndex
this.$data.collectionsType = this.collectionsType
this.displayModalEditFolder(true) this.displayModalEditFolder(true)
this.syncCollections() this.syncCollections()
}, },
editRequest(payload) { editRequest(payload) {
const { collectionIndex, folderIndex, folderName, request, requestIndex } = payload const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
} = payload
this.$data.editingCollectionIndex = collectionIndex this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName this.$data.editingFolderName = folderName
this.$data.editingRequest = request this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex this.$data.editingRequestIndex = requestIndex
this.$emit("select-request", requestIndex)
this.displayModalEditRequest(true) this.displayModalEditRequest(true)
this.syncCollections() this.syncCollections()
}, },
@@ -255,9 +579,86 @@ export default {
) )
} }
}, },
}, expandCollection(collectionID) {
beforeDestroy() { this.teamCollectionAdapter.expandCollection(collectionID)
document.removeEventListener("keydown", this._keyListener) },
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> </script>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 270px);
}
</style>

View File

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

View File

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

View File

@@ -1,7 +1,10 @@
<template> <template>
<div> <div>
<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" draggable="true"
@dragstart="dragStart" @dragstart="dragStart"
@dragover.stop @dragover.stop
@@ -10,21 +13,28 @@
> >
<div> <div>
<button <button
v-tooltip="!doc ? $t('use_request') : ''"
class="icon" class="icon"
@click="!doc ? selectRequest() : {}" @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> <span>{{ request.name }}</span>
</button> </button>
</div> </div>
<v-popover> <v-popover v-if="!saveRequest">
<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> <i class="material-icons">more_vert</i>
</button> </button>
<template slot="popover"> <template slot="popover">
<div> <div>
<button <button
v-close-popover
class="icon" class="icon"
@click=" @click="
$emit('edit-request', { $emit('edit-request', {
@@ -35,14 +45,13 @@
requestIndex, requestIndex,
}) })
" "
v-close-popover
> >
<i class="material-icons">edit</i> <i class="material-icons">edit</i>
<span>{{ $t("edit") }}</span> <span>{{ $t("edit") }}</span>
</button> </button>
</div> </div>
<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> <i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span> <span>{{ $t("delete") }}</span>
</button> </button>
@@ -60,17 +69,19 @@
</template> </template>
<script> <script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default { export default {
props: { props: {
request: Object, request: { type: Object, default: () => {} },
collectionIndex: Number, collectionIndex: { type: Number, default: null },
folderIndex: Number, folderIndex: { type: Number, default: null },
folderName: String, folderName: { type: String, default: null },
requestIndex: Number, // eslint-disable-next-line vue/require-default-prop
requestIndex: [Number, String],
doc: Boolean, doc: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
picked: { type: Object, default: () => {} },
}, },
data() { data() {
return { return {
@@ -85,22 +96,30 @@ export default {
confirmRemove: false, confirmRemove: false,
} }
}, },
subscriptions() { computed: {
return { isSelected() {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"), return (
} this.picked &&
this.picked.pickedType === "my-request" &&
this.picked.folderPath === this.folderPath &&
this.picked.requestIndex === this.requestIndex
)
},
}, },
methods: { methods: {
syncCollections() {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
},
selectRequest() { 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 }) { dragStart({ dataTransfer }) {
this.dragging = !this.dragging this.dragging = !this.dragging
@@ -110,20 +129,17 @@ export default {
dataTransfer.setData("requestIndex", this.$props.requestIndex) dataTransfer.setData("requestIndex", this.$props.requestIndex)
}, },
removeRequest() { removeRequest() {
this.$store.commit("postwoman/removeRequest", { this.$emit("remove-request", {
collectionIndex: this.$props.collectionIndex, collectionIndex: this.$props.collectionIndex,
folderName: this.$props.folderName, folderName: this.$props.folderName,
requestIndex: this.$props.requestIndex, requestIndex: this.$props.requestIndex,
flag: "rest",
}) })
this.$toast.error(this.$t("deleted"), {
icon: "delete",
})
this.confirmRemove = false
this.syncCollections()
}, },
getRequestLabelColor(method) { 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"> <div slot="body" class="flex flex-col">
<label for="selectLabel">{{ $t("label") }}</label> <label for="selectLabel">{{ $t("label") }}</label>
<input <input
type="text"
id="selectLabel" id="selectLabel"
v-model="name" v-model="name"
type="text"
:placeholder="$t('my_new_environment')" :placeholder="$t('my_new_environment')"
@keyup.enter="addNewEnvironment" @keyup.enter="addNewEnvironment"
/> />
@@ -46,18 +46,20 @@ export default {
}, },
data() { data() {
return { return {
name: undefined, name: null,
} }
}, },
subscriptions() { subscriptions() {
return { return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments") SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
} }
}, },
methods: { methods: {
syncEnvironments() { syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) { 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() { addNewEnvironment() {
@@ -65,7 +67,7 @@ export default {
this.$toast.info(this.$t("invalid_environment_name")) this.$toast.info(this.$t("invalid_environment_name"))
return return
} }
let newEnvironment = [ const newEnvironment = [
{ {
name: this.$data.name, name: this.$data.name,
variables: [], variables: [],
@@ -75,12 +77,12 @@ export default {
environments: newEnvironment, environments: newEnvironment,
confirmation: "Environment added", confirmation: "Environment added",
}) })
this.$emit("hide-modal")
this.syncEnvironments() this.syncEnvironments()
this.hideModal()
}, },
hideModal() { hideModal() {
this.name = null
this.$emit("hide-modal") this.$emit("hide-modal")
this.$data.name = undefined
}, },
}, },
} }

View File

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

View File

@@ -8,18 +8,22 @@
</button> </button>
</div> </div>
<v-popover> <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> <i class="material-icons">more_vert</i>
</button> </button>
<template slot="popover"> <template slot="popover">
<div> <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> <i class="material-icons">create</i>
<span>{{ $t("edit") }}</span> <span>{{ $t("edit") }}</span>
</button> </button>
</div> </div>
<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> <i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span> <span>{{ $t("delete") }}</span>
</button> </button>
@@ -42,8 +46,8 @@ import { getSettingSubject } from "~/newstore/settings"
export default { export default {
props: { props: {
environment: Object, environment: { type: Object, default: () => {} },
environmentIndex: Number, environmentIndex: { type: Number, default: null },
}, },
data() { data() {
return { return {
@@ -52,13 +56,15 @@ export default {
}, },
subscriptions() { subscriptions() {
return { return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments") SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
} }
}, },
methods: { methods: {
syncEnvironments() { syncEnvironments() {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) { 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() { removeEnvironment() {

View File

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

View File

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

View File

@@ -28,19 +28,6 @@
</ul> </ul>
</template> </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> <script>
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
@@ -60,3 +47,16 @@ export default {
}, },
} }
</script> </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="flex-col">
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<input <input
v-model="message"
:aria-label="$t('label')" :aria-label="$t('label')"
type="text" type="text"
autofocus autofocus
v-model="message"
:placeholder="$t('paste_a_note')" :placeholder="$t('paste_a_note')"
@keyup.enter="formPost"
class="rounded-t-lg" class="rounded-t-lg"
@keyup.enter="formPost"
/> />
</div> </div>
<div class="border-b show-on-large-screen border-brdColor"> <div class="border-b show-on-large-screen border-brdColor">
<input <input
v-model="label"
:aria-label="$t('label')" :aria-label="$t('label')"
type="text" type="text"
autofocus autofocus
v-model="label"
:placeholder="$t('label')" :placeholder="$t('label')"
@keyup.enter="formPost" @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> <i class="material-icons">add</i>
<span>Add</span> <span>Add</span>
</button> </button>

View File

@@ -1,8 +1,13 @@
<template> <template>
<div> <div>
<div> <div>
<button class="icon" @click="signInWithGoogle" v-close-popover> <button v-close-popover class="icon" @click="signInWithGoogle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons"> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path <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" 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> </button>
</div> </div>
<div> <div>
<button class="icon" @click="signInWithGithub" v-close-popover> <button v-close-popover class="icon" @click="signInWithGithub">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="material-icons"> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="material-icons"
>
<path <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" 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, closeOnSwipe: false,
action: { action: {
text: this.$t("yes"), text: this.$t("yes"),
onClick: (e, toastObject) => { onClick: (_, toastObject) => {
fb.writeSettings("syncHistory", true) fb.writeSettings("syncHistory", true)
fb.writeSettings("syncCollections", true) fb.writeSettings("syncCollections", true)
fb.writeSettings("syncEnvironments", true) fb.writeSettings("syncEnvironments", true)
@@ -96,7 +106,7 @@ export default {
closeOnSwipe: false, closeOnSwipe: false,
action: { action: {
text: this.$t("yes"), text: this.$t("yes"),
onClick: async (e, toastObject) => { onClick: async (_, toastObject) => {
const { user } = await fb.signInWithGithub() const { user } = await fb.signInWithGithub()
await user.linkAndRetrieveDataWithCredential(pendingCred) await user.linkAndRetrieveDataWithCredential(pendingCred)
@@ -111,7 +121,8 @@ export default {
}, },
async signInWithGithub() { async signInWithGithub() {
try { try {
const { credential, additionalUserInfo } = await fb.signInUserWithGithub() const { credential, additionalUserInfo } =
await fb.signInUserWithGithub()
fb.setProviderInfo(credential.providerId, credential.accessToken) fb.setProviderInfo(credential.providerId, credential.accessToken)
@@ -122,7 +133,7 @@ export default {
closeOnSwipe: false, closeOnSwipe: false,
action: { action: {
text: this.$t("yes"), text: this.$t("yes"),
onClick: (e, toastObject) => { onClick: (_, toastObject) => {
fb.writeSettings("syncHistory", true) fb.writeSettings("syncHistory", true)
fb.writeSettings("syncCollections", true) fb.writeSettings("syncCollections", true)
fb.writeSettings("syncEnvironments", true) fb.writeSettings("syncEnvironments", true)
@@ -169,7 +180,7 @@ export default {
closeOnSwipe: false, closeOnSwipe: false,
action: { action: {
text: this.$t("yes"), text: this.$t("yes"),
onClick: async (e, toastObject) => { onClick: async (_, toastObject) => {
const { user } = await fb.signInUserWithGoogle() const { user } = await fb.signInUserWithGoogle()
await user.linkAndRetrieveDataWithCredential(pendingCred) await user.linkAndRetrieveDataWithCredential(pendingCred)

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <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> <i class="material-icons">exit_to_app</i>
<span>{{ $t("logout") }}</span> <span>{{ $t("logout") }}</span>
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import GraphqlCard from "../graphql/Card"
import { mount } from "@vue/test-utils" import { mount } from "@vue/test-utils"
import GraphqlCard from "../graphql/Card"
const factory = (props) => { const factory = (props) => {
return mount(GraphqlCard, { return mount(GraphqlCard, {
@@ -35,20 +35,20 @@ describe("GraphqlCard", () => {
const wrapper = factory({ const wrapper = factory({
entry: { entry: {
type: "graphql", type: "graphql",
url: url, url,
query: query, query,
star: false, star: false,
}, },
}) })
expect(wrapper).toBeTruthy() 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({ const wrapper = factory({
entry: { entry: {
type: "graphql", type: "graphql",
url: url, url,
query: query, query,
star: true, star: true,
}, },
}) })
@@ -61,8 +61,8 @@ describe("GraphqlCard", () => {
const wrapper = factory({ const wrapper = factory({
entry: { entry: {
type: "graphql", type: "graphql",
url: url, url,
query: query, query,
star: true, star: true,
}, },
}) })
@@ -85,12 +85,14 @@ describe("GraphqlCard", () => {
const wrapper = factory({ const wrapper = factory({
entry: { entry: {
type: "graphql", type: "graphql",
url: url, url,
query: query, query,
star: true, 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() expect(wrapper.emitted("use-entry")).toBeTruthy()
}) })
@@ -98,12 +100,14 @@ describe("GraphqlCard", () => {
const wrapper = factory({ const wrapper = factory({
entry: { entry: {
type: "graphql", type: "graphql",
url: url, url,
query: query, query,
star: true, 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() 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 { mount } from "@vue/test-utils"
import RestCard from "../rest/Card"
const factory = (props) => { const factory = (props) => {
return mount(RestCard, { return mount(RestCard, {
@@ -26,7 +26,7 @@ const factory = (props) => {
const url = "https://dummydata.com/get" const url = "https://dummydata.com/get"
const entry = { const entry = {
type: "rest", type: "rest",
url: url, url,
method: "GET", method: "GET",
status: 200, status: 200,
star: false, star: false,
@@ -37,7 +37,7 @@ describe("RestCard", () => {
expect(wrapper).toBeTruthy() 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 }) const wrapper = factory({ entry })
wrapper.find("button[data-testid='star_button']").trigger("click") 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 () => { test("use-entry emit on clicking the restore button", async () => {
const wrapper = factory({ entry }) 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() expect(wrapper.emitted("use-entry")).toBeTruthy()
}) })
test("delete-entry emit on clicking the delete button", async () => { test("delete-entry emit on clicking the delete button", async () => {
const wrapper = factory({ entry }) 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() expect(wrapper.emitted("delete-entry")).toBeTruthy()
}) })
}) })

View File

@@ -13,42 +13,46 @@
/> />
</li> </li>
<button <button
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
data-testid="star_button" data-testid="star_button"
class="icon" class="icon"
:class="{ stared: entry.star }" :class="{ stared: entry.star }"
@click="$emit('toggle-star')" @click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
> >
<i class="material-icons"> <i class="material-icons">
{{ entry.star ? "star" : "star_border" }} {{ entry.star ? "star" : "star_border" }}
</i> </i>
</button> </button>
<button <button
data-testid="query_expand"
class="icon"
@click="expand = !expand"
v-tooltip="{ v-tooltip="{
content: expand ? $t('hide_more') : $t('show_more'), content: expand ? $t('hide_more') : $t('show_more'),
}" }"
data-testid="query_expand"
class="icon"
@click="expand = !expand"
> >
<i class="material-icons"> <i class="material-icons">
{{ expand ? "unfold_less" : "unfold_more" }} {{ expand ? "unfold_less" : "unfold_more" }}
</i> </i>
</button> </button>
<v-popover> <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> <i class="material-icons">more_vert</i>
</button> </button>
<template slot="popover"> <template slot="popover">
<div> <div>
<button <button
v-close-popover
data-testid="restore_history_entry" data-testid="restore_history_entry"
class="icon" class="icon"
@click="$emit('use-entry')"
:aria-label="$t('restore')" :aria-label="$t('restore')"
v-close-popover @click="$emit('use-entry')"
> >
<i class="material-icons">restore</i> <i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span> <span>{{ $t("restore") }}</span>
@@ -56,11 +60,11 @@
</div> </div>
<div> <div>
<button <button
v-close-popover
data-testid="delete_history_entry" data-testid="delete_history_entry"
class="icon" class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')" :aria-label="$t('delete')"
v-close-popover @click="$emit('delete-entry')"
> >
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span> <span>{{ $t("delete") }}</span>
@@ -86,11 +90,11 @@
<div v-if="showMore" class="show-on-large-screen"> <div v-if="showMore" class="show-on-large-screen">
<li> <li>
<input <input
v-tooltip="entry.date"
:aria-label="$t('time')" :aria-label="$t('time')"
type="text" type="text"
readonly readonly
:value="entry.time" :value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor" class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/> />
</li> </li>
@@ -119,24 +123,10 @@
</div> </div>
</template> </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> <script>
export default { export default {
props: { props: {
entry: Object, entry: { type: Object, default: () => {} },
showMore: Boolean, showMore: Boolean,
}, },
data() { data() {
@@ -153,3 +143,17 @@ export default {
}, },
} }
</script> </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> <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"> <div class="show-on-large-screen">
<input <input
v-model="filterText"
aria-label="Search" aria-label="Search"
type="search" type="search"
:placeholder="$t('search')" :placeholder="$t('search')"
v-model="filterText"
class="rounded-t-lg" class="rounded-t-lg"
/> />
</div> </div>
@@ -16,9 +16,9 @@
<ul v-for="(entry, index) in filteredHistory" :key="`entry-${index}`"> <ul v-for="(entry, index) in filteredHistory" :key="`entry-${index}`">
<HistoryRestCard <HistoryRestCard
v-if="page == 'rest'" v-if="page == 'rest'"
:entry="entry"
:id="index" :id="index"
:showMore="showMore" :entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)" @toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)" @delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)" @use-entry="useHistory(entry)"
@@ -26,21 +26,24 @@
<HistoryGraphqlCard <HistoryGraphqlCard
v-if="page == 'graphql'" v-if="page == 'graphql'"
:entry="entry" :entry="entry"
:showMore="showMore" :show-more="showMore"
@toggle-star="toggleStar(entry)" @toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)" @delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)" @use-entry="useHistory(entry)"
/> />
</ul> </ul>
</div> </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 }}" {{ $t("nothing_found") }} "{{ filterText }}"
</p> </p>
<p v-if="history.length === 0" class="info"> <p v-if="history.length === 0" class="info">
<i class="material-icons">schedule</i> {{ $t("history_empty") }} <i class="material-icons">schedule</i> {{ $t("history_empty") }}
</p> </p>
<div v-if="history.length !== 0" class="rounded-b-lg bg-bgDarkColor"> <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 <button
data-testid="clear_history" data-testid="clear_history"
class="icon" class="icon"
@@ -60,22 +63,24 @@
</i> </i>
</button> </button>
</div> </div>
<div class="row-wrapper" v-else> <div v-else class="row-wrapper">
<p class="info"><i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}</p> <p class="info">
<i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}
</p>
<div> <div>
<button <button
v-tooltip="$t('yes')"
data-testid="confirm_clear_history" data-testid="confirm_clear_history"
class="icon" class="icon"
@click="clearHistory" @click="clearHistory"
v-tooltip="$t('yes')"
> >
<i class="material-icons">done</i> <i class="material-icons">done</i>
</button> </button>
<button <button
v-tooltip="$t('no')"
data-testid="reject_clear_history" data-testid="reject_clear_history"
class="icon" class="icon"
@click="disableHistoryClearing" @click="disableHistoryClearing"
v-tooltip="$t('no')"
> >
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
@@ -85,6 +90,89 @@
</AppSection> </AppSection>
</template> </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"> <style scoped lang="scss">
.virtual-list { .virtual-list {
max-height: calc(100vh - 270px); max-height: calc(100vh - 270px);
@@ -105,106 +193,3 @@ ol {
} }
} }
</style> </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> </li>
<span> <span>
<button <button
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
data-testid="star_button" data-testid="star_button"
class="icon" class="icon"
:class="{ stared: entry.star }" :class="{ stared: entry.star }"
@click="$emit('toggle-star')" @click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
> >
<i class="material-icons"> <i class="material-icons">
{{ entry.star ? "star" : "star_border" }} {{ entry.star ? "star" : "star_border" }}
@@ -48,17 +48,17 @@
</button> </button>
</li> --> </li> -->
<v-popover> <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> <i class="material-icons">more_vert</i>
</button> </button>
<template slot="popover"> <template slot="popover">
<div> <div>
<button <button
v-close-popover
data-testid="restore_history_entry" data-testid="restore_history_entry"
class="icon" class="icon"
@click="$emit('use-entry')"
:aria-label="$t('edit')" :aria-label="$t('edit')"
v-close-popover @click="$emit('use-entry')"
> >
<i class="material-icons">restore</i> <i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span> <span>{{ $t("restore") }}</span>
@@ -66,11 +66,11 @@
</div> </div>
<div> <div>
<button <button
v-close-popover
data-testid="delete_history_entry" data-testid="delete_history_entry"
class="icon" class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')" :aria-label="$t('delete')"
v-close-popover @click="$emit('delete-entry')"
> >
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span> <span>{{ $t("delete") }}</span>
@@ -95,11 +95,11 @@
<div v-if="showMore" class="show-on-large-screen"> <div v-if="showMore" class="show-on-large-screen">
<li> <li>
<input <input
v-tooltip="entry.date"
:aria-label="$t('time')" :aria-label="$t('time')"
type="text" type="text"
readonly readonly
:value="entry.time" :value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor" class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/> />
</li> </li>
@@ -128,26 +128,12 @@
</div> </div>
</template> </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> <script>
import findStatusGroup from "~/helpers/findStatusGroup" import findStatusGroup from "~/helpers/findStatusGroup"
export default { export default {
props: { props: {
entry: Object, entry: { type: Object, default: () => {} },
showMore: Boolean, showMore: Boolean,
}, },
data() { data() {
@@ -167,3 +153,17 @@ export default {
}, },
} }
</script> </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> <label for="reqParamList">{{ $t("request_body") }}</label>
<div> <div>
<button <button
v-tooltip.bottom="$t('clear')"
class="icon" class="icon"
@click="clearContent('bodyParams', $event)" @click="clearContent('bodyParams', $event)"
v-tooltip.bottom="$t('clear')"
> >
<i class="material-icons">clear_all</i> <i class="material-icons">clear_all</i>
</button> </button>
@@ -19,7 +19,14 @@
<ul <ul
v-for="(param, index) in bodyParams" v-for="(param, index) in bodyParams"
:key="index" :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 }" :class="{ 'border-t': index == 0 }"
> >
<li> <li>
@@ -27,9 +34,9 @@
:placeholder="`key ${index + 1}`" :placeholder="`key ${index + 1}`"
:name="`bparam ${index}`" :name="`bparam ${index}`"
:value="param.key" :value="param.key"
autofocus
@change="updateBodyParams($event, index, `setKeyBodyParams`)" @change="updateBodyParams($event, index, `setKeyBodyParams`)"
@keyup.prevent="setRouteQueryState" @keyup.prevent="setRouteQueryState"
autofocus
/> />
</li> </li>
<li> <li>
@@ -59,8 +66,6 @@
<div> <div>
<li> <li>
<button <button
class="icon"
@click="toggleActive(index,param)"
v-tooltip.bottom="{ v-tooltip.bottom="{
content: param.hasOwnProperty('active') content: param.hasOwnProperty('active')
? param.active ? param.active
@@ -68,6 +73,8 @@
: $t('turn_on') : $t('turn_on')
: $t('turn_off'), : $t('turn_off'),
}" }"
class="icon"
@click="toggleActive(index, param)"
> >
<i class="material-icons"> <i class="material-icons">
{{ {{
@@ -84,7 +91,10 @@
<div v-if="contentType === 'multipart/form-data'"> <div v-if="contentType === 'multipart/form-data'">
<li> <li>
<label for="attachment" class="p-0"> <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> <i class="material-icons">attach_file</i>
</button> </button>
</label> </label>
@@ -92,17 +102,17 @@
ref="attachment" ref="attachment"
name="attachment" name="attachment"
type="file" type="file"
@change="setRequestAttachment($event, index)"
multiple multiple
@change="setRequestAttachment($event, index)"
/> />
</li> </li>
</div> </div>
<div> <div>
<li> <li>
<button <button
v-tooltip.bottom="$t('delete')"
class="icon" class="icon"
@click="removeRequestBodyParam(index)" @click="removeRequestBodyParam(index)"
v-tooltip.bottom="$t('delete')"
> >
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
</button> </button>
@@ -111,7 +121,7 @@
</ul> </ul>
<ul> <ul>
<li> <li>
<button class="icon" @click="addRequestBodyParam" name="addrequest"> <button class="icon" name="addrequest" @click="addRequestBodyParam">
<i class="material-icons">add</i> <i class="material-icons">add</i>
<span>{{ $t("add_new") }}</span> <span>{{ $t("add_new") }}</span>
</button> </button>
@@ -120,26 +130,16 @@
</div> </div>
</template> </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> <script>
export default { export default {
props: { props: {
bodyParams: { type: Array, default: () => [] }, bodyParams: { type: Array, default: () => [] },
}, },
computed: {
contentType() {
return this.$store.state.request.contentType
},
},
methods: { methods: {
clearContent(bodyParams, $event) { clearContent(bodyParams, $event) {
this.$emit("clear-content", bodyParams, $event) this.$emit("clear-content", bodyParams, $event)
@@ -148,8 +148,13 @@ export default {
this.$emit("set-route-query-state") this.$emit("set-route-query-state")
}, },
removeRequestBodyParam(index) { removeRequestBodyParam(index) {
const paramArr = this.$store.state.request.bodyParams const paramArr = this.$store.state.request.bodyParams.filter(
.filter((item, itemIndex) => itemIndex !== index && (item.hasOwnProperty("active") ? item.active == true : true)) (item, itemIndex) =>
itemIndex !== index &&
(Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true)
)
this.setRawParams(paramArr) this.setRawParams(paramArr)
this.$emit("remove-request-body-param", index) this.$emit("remove-request-body-param", index)
}, },
@@ -174,49 +179,70 @@ export default {
fileIndex, fileIndex,
}) })
}, },
updateBodyParams(event, index, type){ updateBodyParams(event, index, type) {
this.$store.commit(type, { this.$store.commit(type, {
index, index,
value: event.target.value, value: event.target.value,
}) })
let paramArr = this.$store.state.request.bodyParams const paramArr = this.$store.state.request.bodyParams.filter((item) =>
.filter((item) => (item.hasOwnProperty("active") ? item.active == true : true)) Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
this.setRawParams(paramArr) : true
)
this.setRawParams(paramArr)
}, },
toggleActive(index, param){ toggleActive(index, param) {
let paramArr = this.$store.state.request.bodyParams const paramArr = this.$store.state.request.bodyParams.filter(
.filter((item, itemIndex) => { (item, itemIndex) => {
if(index === itemIndex){ if (index === itemIndex) {
return !param.active return !param.active
} else { } else {
return item.hasOwnProperty("active") ? item.active == true : true return Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true
} }
}) }
)
this.setRawParams(paramArr) this.setRawParams(paramArr)
this.$store.commit('setActiveBodyParams', { this.$store.commit("setActiveBodyParams", {
index, index,
value: param.hasOwnProperty('active') ? !param.active : false, value: Object.prototype.hasOwnProperty.call(param, "active")
? !param.active
: false,
}) })
}, },
setRawParams(filteredParamArr){ setRawParams(filteredParamArr) {
let rawParams = {} let rawParams = {}
filteredParamArr.forEach(_param=>{ filteredParamArr.forEach((_param) => {
rawParams={ rawParams = {
...rawParams, ...rawParams,
[_param.key]:_param.value [_param.key]: _param.value,
} }
}) })
const rawParamsStr = JSON.stringify(rawParams,null,2) const rawParamsStr = JSON.stringify(rawParams, null, 2)
this.$store.commit("setState", { value: rawParamsStr, attribute: "rawParams" }) this.$store.commit("setState", {
} value: rawParamsStr,
}, attribute: "rawParams",
computed: { })
contentType() {
return this.$store.state.request.contentType
}, },
}, },
} }
</script> </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> </div>
<div slot="body" class="flex flex-col"> <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"> <span class="select-wrapper">
<v-popover> <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 <input
v-else v-else
id="requestType" id="requestType"
@@ -26,7 +28,11 @@
/> />
<template slot="popover"> <template slot="popover">
<div v-for="gen in codegens" :key="gen.id"> <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 }} {{ gen.name }}
</button> </button>
</div> </div>
@@ -37,24 +43,31 @@
<label for="generatedCode">{{ $t("generated_code") }}</label> <label for="generatedCode">{{ $t("generated_code") }}</label>
<div> <div>
<button <button
class="icon"
@click="copyRequestCode"
ref="copyRequestCode" ref="copyRequestCode"
v-tooltip="$t('copy_code')" v-tooltip="$t('copy_code')"
class="icon"
@click="copyRequestCode"
> >
<i class="material-icons">content_copy</i> <i class="material-icons">content_copy</i>
</button> </button>
</div> </div>
</div> </div>
<textarea <SmartAceEditor
id="generatedCode" v-if="requestType"
ref="generatedCode" ref="generatedCode"
name="generatedCode" :value="requestCode"
rows="8" :lang="codegens.find((x) => x.id === requestType).language"
v-model="requestCode" :options="{
readonly maxLines: '10',
class="rounded-b-lg" minLines: '10',
></textarea> fontSize: '16px',
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="rounded-b-lg"
/>
</div> </div>
</SmartModal> </SmartModal>
</template> </template>
@@ -65,8 +78,8 @@ import { codegens } from "~/helpers/codegen/codegen"
export default { export default {
props: { props: {
show: Boolean, show: Boolean,
requestCode: String, requestCode: { type: String, default: null },
requestTypeProp: { type: String, default: "" }, requestTypeProp: { type: String, default: "curl" },
}, },
data() { data() {
return { return {
@@ -97,9 +110,13 @@ export default {
this.$toast.success(this.$t("copied_to_clipboard"), { this.$toast.success(this.$t("copied_to_clipboard"), {
icon: "done", icon: "done",
}) })
this.$refs.generatedCode.select() this.$refs.generatedCode.editor.selectAll()
this.$refs.generatedCode.editor.focus()
document.execCommand("copy") 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> <template>
<AppSection label="Headers" ref="headers" no-legend> <AppSection ref="headers" label="Headers" no-legend>
<ul v-if="headers.length !== 0"> <ul v-if="headers.length !== 0">
<li> <li>
<div class="row-wrapper"> <div class="row-wrapper">
<label for="headerList">{{ $t("header_list") }}</label> <label for="headerList">{{ $t("header_list") }}</label>
<div> <div>
<button <button
v-tooltip.bottom="$t('clear')"
class="icon" class="icon"
@click="clearContent('headers', $event)" @click="clearContent('headers', $event)"
v-tooltip.bottom="$t('clear')"
> >
<i class="material-icons">clear_all</i> <i class="material-icons">clear_all</i>
</button> </button>
@@ -19,7 +19,14 @@
<ul <ul
v-for="(header, index) in headers" v-for="(header, index) in headers"
:key="`${header.value}_${index}`" :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 }" :class="{ 'border-t': index == 0 }"
> >
<li> <li>
@@ -28,6 +35,7 @@
:source="commonHeaders" :source="commonHeaders"
:spellcheck="false" :spellcheck="false"
:value="header.key" :value="header.key"
autofocus
@input=" @input="
$store.commit('setKeyHeader', { $store.commit('setKeyHeader', {
index, index,
@@ -35,7 +43,6 @@
}) })
" "
@keyup.prevent="setRouteQueryState" @keyup.prevent="setRouteQueryState"
autofocus
/> />
</li> </li>
<li> <li>
@@ -55,13 +62,6 @@
<div> <div>
<li> <li>
<button <button
class="icon"
@click="
$store.commit('setActiveHeader', {
index,
value: header.hasOwnProperty('active') ? !header.active : false,
})
"
v-tooltip.bottom="{ v-tooltip.bottom="{
content: header.hasOwnProperty('active') content: header.hasOwnProperty('active')
? header.active ? header.active
@@ -69,6 +69,13 @@
: $t('turn_on') : $t('turn_on')
: $t('turn_off'), : $t('turn_off'),
}" }"
class="icon"
@click="
$store.commit('setActiveHeader', {
index,
value: header.hasOwnProperty('active') ? !header.active : false,
})
"
> >
<i class="material-icons"> <i class="material-icons">
{{ {{
@@ -84,7 +91,11 @@
</div> </div>
<div> <div>
<li> <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> <i class="material-icons">delete</i>
</button> </button>
</li> </li>

View File

@@ -11,7 +11,12 @@
</div> </div>
</div> </div>
<div slot="body" class="flex flex-col"> <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>
<div slot="footer"> <div slot="footer">
<div class="row-wrapper"> <div class="row-wrapper">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,12 @@
<template> <template>
<div> <div>
<p v-for="(value, key) in headers" :key="key"> <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> </p>
</div> </div>
</template> </template>
@@ -9,7 +14,7 @@
<script> <script>
export default { export default {
props: { props: {
headers: {}, headers: { type: Object, default: () => {} },
}, },
} }
</script> </script>

View File

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

View File

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