Compare commits
1 Commits
v2.1.0
...
feat/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da368a2d72 |
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@@ -2,12 +2,13 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -16,13 +17,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Install pnpm
|
||||
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: pnpm
|
||||
- name: Cache .pnpm-store
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
- name: Install pnpm
|
||||
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||
- name: Run tests
|
||||
run: pnpm i && pnpm -r test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -109,4 +109,4 @@ tests/*/videos
|
||||
.netlify
|
||||
|
||||
# Andrew's crazy Volar shim generator
|
||||
shims-volar.d.ts
|
||||
shims-volar.d.ts
|
||||
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -15,7 +15,7 @@ COPY . .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN pnpm i --unsafe-perm=true
|
||||
RUN pnpm i
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
EXPOSE 3000
|
||||
|
||||
25
README.md
25
README.md
@@ -89,7 +89,7 @@
|
||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
||||
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||
|
||||
🌈 **Make it yours:** Customizable combinations for background, foreground and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||
🌈 **Make it yours:** Customizable combinations for background, foreground and accent colors — [customize now ✨](https://hoppscotch.io/settings).
|
||||
|
||||
**Theming**
|
||||
|
||||
@@ -256,12 +256,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
|
||||
👨👩👧👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
|
||||
|
||||
- Unlimited teams
|
||||
- Unlimited shared collections
|
||||
- Unlimited team collections and shared requests
|
||||
- Unlimited team members
|
||||
- Role-based access control
|
||||
- Cloud sync
|
||||
- Multiple devices
|
||||
- User roles
|
||||
|
||||
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||
|
||||
@@ -293,7 +290,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
||||
|
||||
## **Developing**
|
||||
|
||||
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/.env.example) file found in `packages/hoppscotch-app` with your own keys and rename it to `.env`.
|
||||
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in `packages/hoppscotch-app` with your own keys and rename it to `.env`.
|
||||
|
||||
_Sample keys only works with the [production build](https://hoppscotch.io)._
|
||||
|
||||
@@ -305,10 +302,9 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
|
||||
### Local development environment
|
||||
|
||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
||||
2. Install pnpm using npm by running `npm install -g pnpm`.
|
||||
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
4. Start the development server with `pnpm run dev`.
|
||||
5. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
||||
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
3. Start the development server with `pnpm run dev`.
|
||||
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
|
||||
|
||||
### Docker compose
|
||||
|
||||
@@ -327,10 +323,9 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
|
||||
## **Releasing**
|
||||
|
||||
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
|
||||
2. Install pnpm using npm by running `npm install -g pnpm`.
|
||||
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
4. Build the release files with `pnpm run generate`.
|
||||
5. Find the built project in `packages/hoppscotch-app/dist`.
|
||||
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
|
||||
3. Build the release files with `pnpm run generate`.
|
||||
4. Find the built project in `packages/hoppscotch-app/dist`.
|
||||
|
||||
## **Contributing**
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ if there is no existing translation, you can create a new one by following these
|
||||
|
||||
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||
2. **Create a new branch for your translation.**
|
||||
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-app/locales) directory.**
|
||||
4. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/locales/en.json) to the target language file.**
|
||||
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/locales) directory.**
|
||||
4. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/locales/en.json) to the target language file.**
|
||||
5. **Translate the strings in the target language file.**
|
||||
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/languages.json).**
|
||||
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/languages.json).**
|
||||
7. **Save & commit changes.**
|
||||
8. **Send a pull request.**
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"hosting": {
|
||||
"predeploy": [
|
||||
"cd packages/hoppscotch-app && mv .env.example .env && cd ../.. && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||
"cd packages/hoppscotch-app && mv .env.example .env && npm install -g pnpm && pnpm i && pnpm run generate"
|
||||
],
|
||||
"public": "packages/hoppscotch-app/dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
|
||||
14
netlify.toml
14
netlify.toml
@@ -7,12 +7,6 @@
|
||||
publish = "packages/hoppscotch-app/dist"
|
||||
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
|
||||
[[redirects]]
|
||||
from = "/discord"
|
||||
to = "https://discord.gg/GAMWxmR"
|
||||
@@ -33,7 +27,7 @@
|
||||
|
||||
[[redirects]]
|
||||
from = "/careers"
|
||||
to = "https://hoppscotch.notion.site/3b9d5d5239a043bfb91701faabf5b8f0"
|
||||
to = "https://www.notion.so/hoppscotch/3b9d5d5239a043bfb91701faabf5b8f0"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
@@ -48,9 +42,3 @@
|
||||
to = "https://twitter.com/hoppscotch_io"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/github"
|
||||
to = "https://github.com/hoppscotch/hoppscotch"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
12
package.json
12
package.json
@@ -10,20 +10,18 @@
|
||||
"dev": "pnpm -r do-dev",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
"start": "pnpm -r do-prod-start",
|
||||
"lint": "pnpm -r do-lint",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
"pre-commit": "pnpm -r do-lint",
|
||||
"test": "pnpm -r do-test"
|
||||
"pre-commit": "pnpm -r do-lintfix"
|
||||
},
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^11.2.6"
|
||||
"husky": "^7.0.2",
|
||||
"lint-staged": "^11.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^13.2.1",
|
||||
"@commitlint/config-conventional": "^13.2.0"
|
||||
"@commitlint/cli": "^13.1.0",
|
||||
"@commitlint/config-conventional": "^13.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ module.exports = {
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:nuxt/recommended",
|
||||
],
|
||||
ignorePatterns: [
|
||||
"helpers/backend/graphql.ts"
|
||||
],
|
||||
plugins: ["vue", "prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
||||
6
packages/hoppscotch-app/.gitignore
vendored
6
packages/hoppscotch-app/.gitignore
vendored
@@ -110,9 +110,3 @@ tests/*/videos
|
||||
|
||||
# Andrew's crazy Volar shim generator
|
||||
shims-volar.d.ts
|
||||
|
||||
# Hoppscotch Backend Schema Introspection JSON
|
||||
helpers/backend/backend-schema.json
|
||||
|
||||
# GraphQL Type Generation
|
||||
helpers/backend/graphql.ts
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><g><rect fill="none" height="24" width="24"/></g><g><path d="M23,12l-2.44-2.79l0.34-3.69l-3.61-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,12l2.44,2.79l-0.34,3.7 l3.61,0.82L8.6,22.5l3.4-1.47l3.4,1.46l1.89-3.19l3.61-0.82l-0.34-3.69L23,12z M10.09,16.72l-3.8-3.81l1.48-1.48l2.32,2.33 l5.85-5.87l1.48,1.48L10.09,16.72z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 484 B |
@@ -71,7 +71,6 @@ body {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -96,15 +95,12 @@ body {
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
@apply flex-shrink-0;
|
||||
|
||||
font-size: var(--body-line-height) !important;
|
||||
width: var(--body-line-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-icons {
|
||||
@apply flex-shrink-0;
|
||||
|
||||
height: var(--body-line-height);
|
||||
width: var(--body-line-height);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="EXPAND_NAVIGATION ? $t('hide.sidebar') : $t('show.sidebar')"
|
||||
:title="LEFT_SIDEBAR ? $t('hide.sidebar') : $t('show.sidebar')"
|
||||
svg="sidebar"
|
||||
class="transform"
|
||||
:class="{ '-rotate-180': !EXPAND_NAVIGATION }"
|
||||
@click.native="EXPAND_NAVIGATION = !EXPAND_NAVIGATION"
|
||||
:class="{ 'transform -rotate-180': !LEFT_SIDEBAR }"
|
||||
@click.native="LEFT_SIDEBAR = !LEFT_SIDEBAR"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -134,21 +133,14 @@
|
||||
:class="{ 'rotate-90': !COLUMN_LAYOUT }"
|
||||
@click.native="COLUMN_LAYOUT = !COLUMN_LAYOUT"
|
||||
/>
|
||||
<span
|
||||
class="transform transition"
|
||||
:class="{
|
||||
'rotate-180': SIDEBAR_ON_LEFT,
|
||||
}"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="SIDEBAR ? $t('hide.sidebar') : $t('show.sidebar')"
|
||||
svg="sidebar"
|
||||
class="transform"
|
||||
:class="{ 'rotate-180': !SIDEBAR }"
|
||||
@click.native="SIDEBAR = !SIDEBAR"
|
||||
/>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="RIGHT_SIDEBAR ? $t('hide.sidebar') : $t('show.sidebar')"
|
||||
svg="sidebar"
|
||||
class="transform rotate-180"
|
||||
:class="{ 'rotate-360': !RIGHT_SIDEBAR }"
|
||||
@click.native="RIGHT_SIDEBAR = !RIGHT_SIDEBAR"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
@@ -176,11 +168,10 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
return {
|
||||
EXPAND_NAVIGATION: useSetting("EXPAND_NAVIGATION"),
|
||||
SIDEBAR: useSetting("SIDEBAR"),
|
||||
LEFT_SIDEBAR: useSetting("LEFT_SIDEBAR"),
|
||||
RIGHT_SIDEBAR: useSetting("RIGHT_SIDEBAR"),
|
||||
ZEN_MODE: useSetting("ZEN_MODE"),
|
||||
COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
|
||||
SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
|
||||
|
||||
navigatorShare: !!navigator.share,
|
||||
|
||||
@@ -190,7 +181,7 @@ export default defineComponent({
|
||||
},
|
||||
watch: {
|
||||
ZEN_MODE() {
|
||||
this.EXPAND_NAVIGATION = !this.ZEN_MODE
|
||||
this.LEFT_SIDEBAR = !this.ZEN_MODE
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
v-for="(shortcut, shortcutIndex) in searchResults"
|
||||
:key="`shortcut-${shortcutIndex}`"
|
||||
:ref="`item-${shortcutIndex}`"
|
||||
:active="shortcutIndex === selectedEntry"
|
||||
:shortcut="shortcut.item"
|
||||
@action="$emit('action', shortcut.item.action)"
|
||||
@mouseover.native="selectedEntry = shortcutIndex"
|
||||
/>
|
||||
<div
|
||||
v-if="searchResults.length === 0"
|
||||
@@ -22,20 +20,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||
import { computed } from "@nuxtjs/composition-api"
|
||||
import Fuse from "fuse.js"
|
||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||
import { HoppAction } from "~/helpers/actions"
|
||||
|
||||
const props = defineProps<{
|
||||
input: Record<string, any>[]
|
||||
search: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: HoppAction): void
|
||||
}>()
|
||||
|
||||
const options = {
|
||||
keys: ["keys", "label", "action", "tags"],
|
||||
}
|
||||
@@ -43,24 +35,4 @@ const options = {
|
||||
const fuse = new Fuse(props.input, options)
|
||||
|
||||
const searchResults = computed(() => fuse.search(props.search))
|
||||
|
||||
const searchResultsItems = computed(() =>
|
||||
searchResults.value.map((searchResult: any) => searchResult.item)
|
||||
)
|
||||
|
||||
const emitSearchAction = (action: HoppAction) => emit("action", action)
|
||||
|
||||
const { bindArrowKeysListerners, unbindArrowKeysListerners, selectedEntry } =
|
||||
useArrowKeysNavigation(searchResultsItems, {
|
||||
onEnter: emitSearchAction,
|
||||
stopPropagation: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
bindArrowKeysListerners()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbindArrowKeysListerners()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -47,140 +47,120 @@
|
||||
:label="$t('header.login')"
|
||||
@click.native="showLogin = true"
|
||||
/>
|
||||
<div v-else class="space-x-2 inline-flex items-center">
|
||||
<ButtonPrimary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('team.invite_tooltip')"
|
||||
:label="$t('team.invite')"
|
||||
svg="user-plus"
|
||||
class="
|
||||
!bg-green-500
|
||||
!text-green-500
|
||||
!bg-opacity-10
|
||||
!hover:bg-opacity-10 !hover:text-green-400 !hover:bg-green-400
|
||||
"
|
||||
@click.native="showTeamsModal = true"
|
||||
/>
|
||||
<span class="px-2">
|
||||
<tippy ref="user" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ProfilePicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="currentUser.displayName"
|
||||
:title="currentUser.displayName"
|
||||
indicator
|
||||
:indicator-styles="isOnLine ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('header.account')"
|
||||
class="rounded"
|
||||
svg="user"
|
||||
/>
|
||||
</template>
|
||||
<SmartItem
|
||||
to="/profile"
|
||||
<span v-else class="px-2">
|
||||
<tippy ref="user" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ProfilePicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="currentUser.displayName"
|
||||
:title="currentUser.displayName"
|
||||
indicator
|
||||
:indicator-styles="isOnLine ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('header.account')"
|
||||
class="rounded"
|
||||
svg="user"
|
||||
:label="$t('navigation.profile')"
|
||||
@click.native="$refs.user.tippy().hide()"
|
||||
/>
|
||||
<SmartItem
|
||||
to="/settings"
|
||||
svg="settings"
|
||||
:label="$t('navigation.settings')"
|
||||
@click.native="$refs.user.tippy().hide()"
|
||||
/>
|
||||
<FirebaseLogout @confirm-logout="$refs.user.tippy().hide()" />
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartItem
|
||||
to="/settings"
|
||||
svg="settings"
|
||||
:label="$t('navigation.settings')"
|
||||
@click.native="$refs.user.tippy().hide()"
|
||||
/>
|
||||
<FirebaseLogout @confirm-logout="$refs.user.tippy().hide()" />
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<AppAnnouncement v-if="!isOnLine" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
<AppSupport :show="showSupport" @hide-modal="showSupport = false" />
|
||||
<AppPowerSearch :show="showSearch" @hide-modal="showSearch = false" />
|
||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, useContext } from "@nuxtjs/composition-api"
|
||||
<script>
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||
import intializePwa from "~/helpers/pwa"
|
||||
import { probableUser$ } from "~/helpers/fb/auth"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const showSupport = ref(false)
|
||||
const showSearch = ref(false)
|
||||
|
||||
/**
|
||||
* Once the PWA code is initialized, this holds a method
|
||||
* that can be called to show the user the installation
|
||||
* prompt.
|
||||
*/
|
||||
const showInstallPrompt = ref(() => Promise.resolve()) // Async no-op till it is initialized
|
||||
|
||||
const showSupport = ref(false)
|
||||
const showSearch = ref(false)
|
||||
const showLogin = ref(false)
|
||||
const showTeamsModal = ref(false)
|
||||
|
||||
const isOnLine = ref(navigator.onLine)
|
||||
|
||||
const currentUser = useReadonlyStream(probableUser$, null)
|
||||
|
||||
defineActionHandler("modals.support.toggle", () => {
|
||||
showSupport.value = !showSupport.value
|
||||
})
|
||||
defineActionHandler("modals.search.toggle", () => {
|
||||
showSearch.value = !showSearch.value
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("online", () => {
|
||||
isOnLine.value = true
|
||||
})
|
||||
window.addEventListener("offline", () => {
|
||||
isOnLine.value = false
|
||||
})
|
||||
|
||||
// Initializes the PWA code - checks if the app is installed,
|
||||
// etc.
|
||||
showInstallPrompt.value = intializePwa()
|
||||
|
||||
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
|
||||
if (!cookiesAllowed) {
|
||||
$toast.show(t("app.we_use_cookies").toString(), {
|
||||
icon: "cookie",
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: t("action.learn_more").toString(),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
window.open("https://docs.hoppscotch.io/privacy", "_blank")?.focus()
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t("action.dismiss").toString(),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
defineActionHandler("modals.support.toggle", () => {
|
||||
showSupport.value = !showSupport.value
|
||||
})
|
||||
}
|
||||
defineActionHandler("modals.search.toggle", () => {
|
||||
showSearch.value = !showSearch.value
|
||||
})
|
||||
|
||||
return {
|
||||
currentUser: useReadonlyStream(currentUser$, null),
|
||||
showSupport,
|
||||
showSearch,
|
||||
}
|
||||
},
|
||||
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,
|
||||
showLogin: false,
|
||||
isOnLine: navigator.onLine,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
window.addEventListener("online", () => {
|
||||
this.isOnLine = true
|
||||
})
|
||||
window.addEventListener("offline", () => {
|
||||
this.isOnLine = false
|
||||
})
|
||||
|
||||
// Initializes the PWA code - checks if the app is installed,
|
||||
// etc.
|
||||
this.showInstallPrompt = await intializePwa()
|
||||
|
||||
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
|
||||
if (!cookiesAllowed) {
|
||||
this.$toast.show(this.$t("app.we_use_cookies"), {
|
||||
icon: "cookie",
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: this.$t("action.learn_more"),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
window
|
||||
.open("https://docs.hoppscotch.io/privacy", "_blank")
|
||||
.focus()
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("action.dismiss"),
|
||||
onClick: (_, toastObject) => {
|
||||
setLocalConfig("cookiesAllowed", "yes")
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
0% {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
100% {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"
|
||||
/>
|
||||
<AppFuse
|
||||
v-if="search && show"
|
||||
v-if="search"
|
||||
:input="fuse"
|
||||
:search="search"
|
||||
@action="runAction"
|
||||
@@ -47,9 +47,7 @@
|
||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||
:shortcut="shortcut"
|
||||
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
|
||||
@action="runAction"
|
||||
@mouseover.native="selectedEntry = shortcutsItems.indexOf(shortcut)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,12 +56,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "@nuxtjs/composition-api"
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { HoppAction, invokeAction } from "~/helpers/actions"
|
||||
import { spotlight as mappings, fuse } from "~/helpers/shortcuts"
|
||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
@@ -82,24 +79,4 @@ const runAction = (command: HoppAction) => {
|
||||
invokeAction(command)
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const shortcutsItems = computed(() =>
|
||||
mappings.reduce(
|
||||
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
const { bindArrowKeysListerners, unbindArrowKeysListerners, selectedEntry } =
|
||||
useArrowKeysNavigation(shortcutsItems, {
|
||||
onEnter: runAction,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) bindArrowKeysListerners()
|
||||
else unbindArrowKeysListerners()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -8,23 +8,33 @@
|
||||
transition
|
||||
items-center
|
||||
group
|
||||
hover:bg-primaryLight
|
||||
focus:outline-none
|
||||
focus-visible:bg-primaryLight
|
||||
search-entry
|
||||
"
|
||||
:class="{ active, 'focus-visible': active }"
|
||||
tabindex="-1"
|
||||
tabindex="0"
|
||||
@click="$emit('action', shortcut.action)"
|
||||
@keydown.enter="$emit('action', shortcut.action)"
|
||||
>
|
||||
<SmartIcon
|
||||
class="mr-4 opacity-50 transition svg-icons group-focus:opacity-100"
|
||||
:class="{ 'opacity-100 text-secondaryDark': active }"
|
||||
class="
|
||||
mr-4
|
||||
opacity-75
|
||||
transition
|
||||
svg-icons
|
||||
group-hover:opacity-100
|
||||
group-focus:opacity-100
|
||||
"
|
||||
:name="shortcut.icon"
|
||||
/>
|
||||
<span
|
||||
class="flex flex-1 mr-4 transition"
|
||||
:class="{ 'text-secondaryDark': active }"
|
||||
class="
|
||||
flex flex-1
|
||||
mr-4
|
||||
transition
|
||||
group-hover:text-secondaryDark
|
||||
group-focus:text-secondaryDark
|
||||
"
|
||||
>
|
||||
{{ $t(shortcut.label) }}
|
||||
</span>
|
||||
@@ -41,32 +51,10 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
shortcut: Object
|
||||
active: Boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-entry {
|
||||
@apply relative;
|
||||
|
||||
&::after {
|
||||
@apply absolute;
|
||||
@apply top-0;
|
||||
@apply left-0;
|
||||
@apply bottom-0;
|
||||
@apply bg-transparent;
|
||||
@apply z-2;
|
||||
@apply w-0.5;
|
||||
@apply transition;
|
||||
|
||||
content: "";
|
||||
}
|
||||
|
||||
&.active::after {
|
||||
@apply bg-accentLight;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<div v-if="navigation.svg">
|
||||
<SmartIcon :name="navigation.svg" class="svg-icons" />
|
||||
</div>
|
||||
<span v-if="EXPAND_NAVIGATION">{{ navigation.title }}</span>
|
||||
<span v-if="LEFT_SIDEBAR">{{ navigation.title }}</span>
|
||||
<tippy
|
||||
v-if="!EXPAND_NAVIGATION"
|
||||
v-if="!LEFT_SIDEBAR"
|
||||
:placement="windowInnerWidth.x.value >= 768 ? 'right' : 'bottom'"
|
||||
theme="tooltip"
|
||||
:content="navigation.title"
|
||||
@@ -35,7 +35,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
windowInnerWidth: useWindowSize(),
|
||||
EXPAND_NAVIGATION: useSetting("EXPAND_NAVIGATION"),
|
||||
LEFT_SIDEBAR: useSetting("LEFT_SIDEBAR"),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
]"
|
||||
:disabled="disabled"
|
||||
:tabindex="loading ? '-1' : '0'"
|
||||
:type="type"
|
||||
>
|
||||
<span
|
||||
v-if="!loading"
|
||||
@@ -144,10 +143,6 @@ export default defineComponent({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "button",
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
'border border-divider hover:border-dividerDark focus-visible:border-dividerDark':
|
||||
outline,
|
||||
},
|
||||
{ '!bg-primaryDark': filled },
|
||||
{ 'bg-primaryDark': filled },
|
||||
]"
|
||||
:disabled="disabled"
|
||||
tabindex="0"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:selected="true"
|
||||
/>
|
||||
<SmartTab
|
||||
v-if="currentUser && !doc"
|
||||
v-if="currentUser && currentUser.eaInvited && !doc"
|
||||
:id="'team-collections'"
|
||||
:label="`${$t('collection.team_collections')}`"
|
||||
>
|
||||
|
||||
@@ -412,7 +412,7 @@ export default defineComponent({
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{ $t("request.name") }}
|
||||
</label>
|
||||
</div>
|
||||
<label class="p-4">
|
||||
<label class="px-4 pt-4 pb-4">
|
||||
{{ $t("collection.select_location") }}
|
||||
</label>
|
||||
<CollectionsGraphql
|
||||
|
||||
@@ -97,80 +97,58 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsGraphqlFolder
|
||||
v-for="(folder, index) in collection.folders"
|
||||
:key="`folder-${String(index)}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:folder="folder"
|
||||
:folder-index="index"
|
||||
:folder-path="`${collectionIndex}/${String(index)}`"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:is-filtered="isFiltered"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${String(index)}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:folder-path="`${collectionIndex}`"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
collection.folders.length === 0 && collection.requests.length === 0
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsGraphqlFolder
|
||||
v-for="(folder, index) in collection.folders"
|
||||
:key="`folder-${String(index)}`"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:folder="folder"
|
||||
:folder-index="index"
|
||||
:folder-path="`${collectionIndex}/${String(index)}`"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:is-filtered="isFiltered"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${String(index)}`"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:folder-path="`${collectionIndex}`"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
collection.folders.length === 0 && collection.requests.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -93,83 +93,61 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsGraphqlFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||
:key="`subFolder-${String(subFolderIndex)}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:folder-path="`${folderPath}/${String(subFolderIndex)}`"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:is-filtered="isFiltered"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${String(index)}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-path="folderPath"
|
||||
:folder-name="folder.name"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsGraphqlFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||
:key="`subFolder-${String(subFolderIndex)}`"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:folder-path="`${folderPath}/${String(subFolderIndex)}`"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:is-filtered="isFiltered"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${String(index)}`"
|
||||
:picked="picked"
|
||||
:saving-mode="savingMode"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-path="folderPath"
|
||||
:folder-name="folder.name"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -249,7 +249,7 @@ export default defineComponent({
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
|
||||
@@ -70,11 +70,6 @@
|
||||
v-if="collections.length === 0"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.collections") }}
|
||||
</span>
|
||||
|
||||
@@ -108,11 +108,6 @@
|
||||
v-if="filteredCollections.length === 0 && filterText.length === 0"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.collections") }}
|
||||
</span>
|
||||
@@ -511,7 +506,7 @@ export default defineComponent({
|
||||
mutation: gql`
|
||||
mutation CreateChildCollection(
|
||||
$childTitle: String!
|
||||
$collectionID: ID!
|
||||
$collectionID: String!
|
||||
) {
|
||||
createChildCollection(
|
||||
childTitle: $childTitle
|
||||
@@ -618,7 +613,7 @@ export default defineComponent({
|
||||
.mutate({
|
||||
// Query
|
||||
mutation: gql`
|
||||
mutation ($collectionID: ID!) {
|
||||
mutation ($collectionID: String!) {
|
||||
deleteCollection(collectionID: $collectionID)
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -116,87 +116,64 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsMyFolder
|
||||
v-for="(folder, index) in collection.folders"
|
||||
:key="`folder-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
: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)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:folder-path="`${collectionIndex}`"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
@edit-request="editRequest($event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
(collection.folders == undefined ||
|
||||
collection.folders.length === 0) &&
|
||||
(collection.requests == undefined || collection.requests.length === 0)
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsMyFolder
|
||||
v-for="(folder, index) in collection.folders"
|
||||
:key="`folder-${index}`"
|
||||
: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)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="-1"
|
||||
:folder-name="collection.name"
|
||||
:folder-path="`${collectionIndex}`"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:picked="picked"
|
||||
@edit-request="editRequest($event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="$emit('remove-request', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
(collection.folders == undefined ||
|
||||
collection.folders.length === 0) &&
|
||||
(collection.requests == undefined ||
|
||||
collection.requests.length === 0)
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -98,87 +98,65 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsMyFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||
:picked="picked"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@update-team-collections="$emit('update-team-collections')"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="removeRequest"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-name="folder.name"
|
||||
:folder-path="folderPath"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
:picked="picked"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="removeRequest"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsMyFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
:folder="subFolder"
|
||||
:folder-index="subFolderIndex"
|
||||
:collection-index="collectionIndex"
|
||||
:doc="doc"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||
:picked="picked"
|
||||
@add-folder="$emit('add-folder', $event)"
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@update-team-collections="$emit('update-team-collections')"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="removeRequest"
|
||||
/>
|
||||
<CollectionsMyRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
:request="request"
|
||||
:collection-index="collectionIndex"
|
||||
:folder-index="folderIndex"
|
||||
:folder-name="folder.name"
|
||||
:folder-path="folderPath"
|
||||
:request-index="index"
|
||||
:doc="doc"
|
||||
:picked="picked"
|
||||
:save-request="saveRequest"
|
||||
:collections-type="collectionsType"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@remove-request="removeRequest"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -112,87 +112,64 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsTeamsFolder
|
||||
v-for="(folder, index) in collection.children"
|
||||
:key="`folder-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
: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"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
: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"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
(collection.children == undefined ||
|
||||
collection.children.length === 0) &&
|
||||
(collection.requests == undefined || collection.requests.length === 0)
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsTeamsFolder
|
||||
v-for="(folder, index) in collection.children"
|
||||
:key="`folder-${index}`"
|
||||
: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"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
:key="`request-${index}`"
|
||||
: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"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
(collection.children == undefined ||
|
||||
collection.children.length === 0) &&
|
||||
(collection.requests == undefined ||
|
||||
collection.requests.length === 0)
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.collection") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -95,85 +95,63 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showChildren || isFiltered" class="flex">
|
||||
<div v-if="showChildren || isFiltered">
|
||||
<CollectionsTeamsFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.children"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
: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"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
class="border-l border-dividerLight ml-6"
|
||||
: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"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-1
|
||||
transform
|
||||
transition
|
||||
cursor-nsResize
|
||||
ml-5.5
|
||||
bg-dividerLight
|
||||
hover:scale-x-125 hover:bg-dividerDark
|
||||
v-if="
|
||||
(folder.children == undefined || folder.children.length === 0) &&
|
||||
(folder.requests == undefined || folder.requests.length === 0)
|
||||
"
|
||||
@click="toggleShowChildren()"
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
<CollectionsTeamsFolder
|
||||
v-for="(subFolder, subFolderIndex) in folder.children"
|
||||
:key="`subFolder-${subFolderIndex}`"
|
||||
: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"
|
||||
/>
|
||||
<CollectionsTeamsRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
:key="`request-${index}`"
|
||||
: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"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
(folder.children == undefined || folder.children.length === 0) &&
|
||||
(folder.requests == undefined || folder.requests.length === 0)
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
class="
|
||||
border-l border-dividerLight
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
ml-6
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">folder_open</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmartConfirmModal
|
||||
|
||||
@@ -82,18 +82,7 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<SmartIcon class="opacity-75 pb-2" name="layers" />
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.environments") }}
|
||||
</span>
|
||||
|
||||
@@ -41,16 +41,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
svg="copy"
|
||||
:label="`${$t('action.duplicate')}`"
|
||||
@click.native="
|
||||
() => {
|
||||
duplicateEnvironment()
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="!(environmentIndex === 'Global')"
|
||||
svg="trash-2"
|
||||
@@ -76,14 +66,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
||||
import {
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
createEnvironment,
|
||||
setEnvironmentVariables,
|
||||
getGlobalVariables,
|
||||
environmentsStore,
|
||||
} from "~/newstore/environments"
|
||||
import { deleteEnvironment } from "~/newstore/environments"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -106,18 +89,6 @@ export default defineComponent({
|
||||
icon: "delete",
|
||||
})
|
||||
},
|
||||
duplicateEnvironment() {
|
||||
if (this.environmentIndex === "Global") {
|
||||
createEnvironment("Global-Copy")
|
||||
setEnvironmentVariables(
|
||||
environmentsStore.value.environments.length - 1,
|
||||
getGlobalVariables().reduce((gVars, gVar) => {
|
||||
gVars.push({ key: gVar.key, value: gVar.value })
|
||||
return gVars
|
||||
}, [])
|
||||
)
|
||||
} else duplicateEnvironment(this.environmentIndex)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -227,7 +227,7 @@ export default defineComponent({
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
|
||||
@@ -106,11 +106,6 @@
|
||||
v-if="environments.length === 0"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.environments") }}
|
||||
</span>
|
||||
|
||||
@@ -26,11 +26,7 @@
|
||||
@click.native="mode = 'email'"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-if="mode === 'email'"
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<div v-if="mode === 'email'" class="flex flex-col space-y-2">
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="email"
|
||||
@@ -44,6 +40,7 @@
|
||||
required
|
||||
spellcheck="false"
|
||||
autofocus
|
||||
@keyup.enter="signInWithEmail"
|
||||
/>
|
||||
<label for="email">
|
||||
{{ $t("auth.email") }}
|
||||
@@ -51,10 +48,18 @@
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:disabled="
|
||||
form.email.length !== 0
|
||||
? emailRegex.test(form.email)
|
||||
? false
|
||||
: true
|
||||
: true
|
||||
"
|
||||
type="button"
|
||||
:label="`${$t('auth.send_magic_link')}`"
|
||||
@click.native="signInWithEmail"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||
<div class="flex flex-col max-w-md justify-center items-center">
|
||||
<SmartIcon class="h-6 text-accent w-6" name="inbox" />
|
||||
@@ -145,6 +150,8 @@ export default defineComponent({
|
||||
signingInWithGoogle: false,
|
||||
signingInWithGitHub: false,
|
||||
signingInWithEmail: false,
|
||||
emailRegex:
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
|
||||
mode: "sign-in",
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<SmartItem
|
||||
svg="log-out"
|
||||
:label="`${$t('auth.logout')}`"
|
||||
:outline="outline"
|
||||
@click.native="
|
||||
() => {
|
||||
$emit('confirm-logout')
|
||||
@@ -25,12 +24,6 @@ import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { signOutUser } from "~/helpers/fb/auth"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
confirmLogout: false,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
focus-visible:bg-transparent focus-visible:border-dividerDark
|
||||
"
|
||||
:placeholder="$t('request.url')"
|
||||
:disabled="connected"
|
||||
@keyup.enter="onConnectClick"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
|
||||
@@ -243,18 +243,6 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.headers") }}
|
||||
</span>
|
||||
@@ -459,15 +447,12 @@ const runQuery = async () => {
|
||||
icon: "done",
|
||||
})
|
||||
} catch (e: any) {
|
||||
response.value = `${e}`
|
||||
response.value = `${e}. ${t("error.check_console_details")}`
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
$toast.error(
|
||||
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
|
||||
{
|
||||
icon: "error_outline",
|
||||
}
|
||||
)
|
||||
$toast.error(`${e} ${t("error.f12_details")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,20 +42,9 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_comment.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.documentation") }}
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -209,18 +198,7 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
|
||||
@@ -60,11 +60,7 @@
|
||||
v-if="history.length === 0"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/history.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<i class="opacity-75 pb-2 material-icons">schedule</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.history") }}
|
||||
</span>
|
||||
|
||||
@@ -95,11 +95,6 @@
|
||||
v-if="authType === 'none'"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/login.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.authorization") }}
|
||||
</span>
|
||||
@@ -114,19 +109,48 @@
|
||||
</div>
|
||||
<div v-if="authType === 'basic'" class="border-b border-dividerLight flex">
|
||||
<div class="border-r border-dividerLight w-2/3">
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="basicUsername"
|
||||
class="bg-transparent flex flex-1 py-1 px-4"
|
||||
:placeholder="$t('authorization.username')"
|
||||
styles="bg-transparent flex flex-1 py-1 px-4"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="http_basic_user"
|
||||
v-model="basicUsername"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('authorization.username')"
|
||||
name="http_basic_user"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="basicPassword"
|
||||
class="bg-transparent flex flex-1 py-1 px-4"
|
||||
:placeholder="$t('authorization.password')"
|
||||
styles="bg-transparent flex flex-1 py-1 px-4"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="http_basic_passwd"
|
||||
v-model="basicPassword"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('authorization.password')"
|
||||
name="http_basic_passwd"
|
||||
:type="passwordFieldType"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
:svg="passwordFieldType === 'text' ? 'eye' : 'eye-off'"
|
||||
@click.native="switchVisibility"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -157,11 +181,22 @@
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'" class="border-b border-dividerLight flex">
|
||||
<div class="border-r border-dividerLight w-2/3">
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="bearerToken"
|
||||
class="bg-transparent flex flex-1 py-1 px-4"
|
||||
placeholder="Token"
|
||||
styles="bg-transparent flex flex-1 py-1 px-4"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="bearer_token"
|
||||
v-model="bearerToken"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
placeholder="Token"
|
||||
name="bearer_token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,11 +231,22 @@
|
||||
class="border-b border-dividerLight flex"
|
||||
>
|
||||
<div class="border-r border-dividerLight w-2/3">
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="oauth2Token"
|
||||
class="bg-transparent flex flex-1 py-1 px-4"
|
||||
placeholder="Token"
|
||||
styles="bg-transparent flex flex-1 py-1 px-4"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="oauth2_token"
|
||||
v-model="oauth2Token"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
placeholder="Token"
|
||||
name="oauth2_token"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
@@ -235,7 +281,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, Ref } from "@nuxtjs/composition-api"
|
||||
import { computed, defineComponent, Ref, ref } from "@nuxtjs/composition-api"
|
||||
import {
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
@@ -271,6 +317,8 @@ export default defineComponent({
|
||||
|
||||
const URLExcludes = useSetting("URL_EXCLUDES")
|
||||
|
||||
const passwordFieldType = ref("password")
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
authType: "none",
|
||||
@@ -278,6 +326,12 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const switchVisibility = () => {
|
||||
if (passwordFieldType.value === "text")
|
||||
passwordFieldType.value = "password"
|
||||
else passwordFieldType.value = "text"
|
||||
}
|
||||
|
||||
return {
|
||||
auth,
|
||||
authType,
|
||||
@@ -288,7 +342,10 @@ export default defineComponent({
|
||||
bearerToken,
|
||||
oauth2Token,
|
||||
URLExcludes,
|
||||
passwordFieldType,
|
||||
clearContent,
|
||||
switchVisibility,
|
||||
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -65,11 +65,6 @@
|
||||
v-if="contentType == null"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/upload_single_file.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.body") }}
|
||||
</span>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="param.key"
|
||||
:placeholder="`${$t('count.parameter', { count: index + 1 })}`"
|
||||
styles="
|
||||
@@ -62,6 +63,22 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="`${$t('count.parameter', { count: index + 1 })}`"
|
||||
:name="'param' + index"
|
||||
:value="param.key"
|
||||
autofocus
|
||||
@change="
|
||||
updateBodyParam(index, {
|
||||
key: $event.target.value,
|
||||
value: param.value,
|
||||
active: param.active,
|
||||
isFile: param.isFile,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<div v-if="param.isFile" class="file-chips-container hide-scrollbar">
|
||||
<div class="space-x-2 file-chips-wrapper">
|
||||
<SmartDeletableChip
|
||||
@@ -75,6 +92,7 @@
|
||||
</div>
|
||||
<span v-else class="flex flex-1">
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="param.value"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
styles="
|
||||
@@ -93,6 +111,21 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
:name="'value' + index"
|
||||
:value="param.value"
|
||||
@change="
|
||||
updateBodyParam(index, {
|
||||
key: param.key,
|
||||
value: $event.target.value,
|
||||
active: param.active,
|
||||
isFile: param.isFile,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<label for="attachment" class="p-0">
|
||||
@@ -153,11 +186,6 @@
|
||||
v-if="bodyParams.length === 0"
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/upload_single_file.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.body") }}
|
||||
</span>
|
||||
@@ -182,6 +210,7 @@ import {
|
||||
updateFormDataEntry,
|
||||
useRESTRequestBody,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -262,6 +291,7 @@ export default defineComponent({
|
||||
clearContent,
|
||||
setRequestAttachment,
|
||||
chipDelete,
|
||||
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
</tippy>
|
||||
<div class="flex flex-1 justify-between">
|
||||
<label for="generatedCode" class="p-4">
|
||||
<label for="generatedCode" class="px-4 pt-4 pb-4">
|
||||
{{ t("request.generated_code") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
px-4
|
||||
truncate
|
||||
"
|
||||
class="!flex flex-1"
|
||||
:class="{ '!flex flex-1': EXPERIMENTAL_URL_BAR_ENABLED }"
|
||||
@input="
|
||||
updateHeader(index, {
|
||||
key: $event,
|
||||
@@ -78,6 +78,7 @@
|
||||
"
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="header.value"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
styles="
|
||||
@@ -95,6 +96,20 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
:name="'value' + index"
|
||||
:value="header.value"
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
key: header.key,
|
||||
value: $event.target.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -144,18 +159,6 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.headers") }}
|
||||
</span>
|
||||
@@ -182,6 +185,7 @@ import {
|
||||
updateRESTHeader,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { HoppRESTHeader } from "~/helpers/types/HoppRESTRequest"
|
||||
|
||||
@@ -250,4 +254,5 @@ const deleteHeader = (index: number) => {
|
||||
const clearContent = () => {
|
||||
deleteAllRESTHeaders()
|
||||
}
|
||||
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="divide-x divide-dividerLight border-b border-dividerLight flex">
|
||||
<input
|
||||
id="oidcDiscoveryURL"
|
||||
v-model="oidcDiscoveryURL"
|
||||
@@ -9,7 +9,7 @@
|
||||
name="oidcDiscoveryURL"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="divide-x divide-dividerLight border-b border-dividerLight flex">
|
||||
<input
|
||||
id="authURL"
|
||||
v-model="authURL"
|
||||
@@ -18,7 +18,7 @@
|
||||
name="authURL"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="divide-x divide-dividerLight border-b border-dividerLight flex">
|
||||
<input
|
||||
id="accessTokenURL"
|
||||
v-model="accessTokenURL"
|
||||
@@ -27,7 +27,7 @@
|
||||
name="accessTokenURL"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="divide-x divide-dividerLight border-b border-dividerLight flex">
|
||||
<input
|
||||
id="clientID"
|
||||
v-model="clientID"
|
||||
@@ -36,7 +36,7 @@
|
||||
name="clientID"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="divide-x divide-dividerLight border-b border-dividerLight flex">
|
||||
<input
|
||||
id="scope"
|
||||
v-model="scope"
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
class="divide-x divide-dividerLight border-b border-dividerLight flex"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="param.key"
|
||||
:placeholder="`${$t('count.parameter', { count: index + 1 })}`"
|
||||
styles="
|
||||
@@ -72,7 +73,25 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="`${$t('count.parameter', {
|
||||
count: index + 1,
|
||||
})}`"
|
||||
:name="'param' + index"
|
||||
:value="param.key"
|
||||
autofocus
|
||||
@change="
|
||||
updateParam(index, {
|
||||
key: $event.target.value,
|
||||
value: param.value,
|
||||
active: param.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="param.value"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
styles="
|
||||
@@ -90,6 +109,20 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
:name="'value' + index"
|
||||
:value="param.value"
|
||||
@change="
|
||||
updateParam(index, {
|
||||
key: param.key,
|
||||
value: $event.target.value,
|
||||
active: param.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -137,18 +170,6 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_files.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.parameters") }}
|
||||
</span>
|
||||
@@ -176,6 +197,7 @@ import {
|
||||
deleteAllRESTParams,
|
||||
setRESTParams,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import "codemirror/mode/yaml/yaml"
|
||||
|
||||
const {
|
||||
@@ -244,4 +266,6 @@ const deleteParam = (index: number) => {
|
||||
const clearContent = () => {
|
||||
deleteAllRESTParams()
|
||||
}
|
||||
|
||||
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
|
||||
</script>
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
</div>
|
||||
<div class="flex flex-1">
|
||||
<SmartEnvInput
|
||||
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
v-model="newEndpoint"
|
||||
:placeholder="`${$t('request.url')}`"
|
||||
styles="
|
||||
@@ -77,6 +78,32 @@
|
||||
"
|
||||
@enter="newSendRequest()"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="url"
|
||||
v-model="newEndpoint"
|
||||
v-focus
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
rounded-r
|
||||
flex
|
||||
text-secondaryDark
|
||||
w-full
|
||||
min-w-32
|
||||
py-2
|
||||
px-4
|
||||
hover:border-dividerDark
|
||||
focus-visible:bg-transparent focus-visible:border-dividerDark
|
||||
"
|
||||
name="url"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:placeholder="`${$t('request.url')}`"
|
||||
autofocus
|
||||
@keyup.enter="newSendRequest()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
@@ -131,7 +158,7 @@
|
||||
</tippy>
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
class="rounded rounded-r-none ml-2"
|
||||
class="rounded-r-none rounded-l ml-2"
|
||||
:label="
|
||||
windowInnerWidth.x.value >= 768 && COLUMN_LAYOUT
|
||||
? `${$t('request.save')}`
|
||||
@@ -150,11 +177,7 @@
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<ButtonSecondary
|
||||
svg="chevron-down"
|
||||
filled
|
||||
class="rounded rounded-l-none"
|
||||
/>
|
||||
<ButtonSecondary svg="chevron-down" filled class="rounded-r" />
|
||||
</template>
|
||||
<input
|
||||
id="request-name"
|
||||
@@ -209,7 +232,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||
import { isRight } from "fp-ts/lib/Either"
|
||||
import {
|
||||
updateRESTResponse,
|
||||
restEndpoint$,
|
||||
@@ -281,31 +303,25 @@ watch(loading, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const newSendRequest = async () => {
|
||||
const newSendRequest = () => {
|
||||
loading.value = true
|
||||
|
||||
// Double calling is because the function returns a TaskEither than should be executed
|
||||
const streamResult = await runRESTRequest$()()
|
||||
|
||||
// TODO: What if stream fetching failed (script execution errors ?) (isLeft)
|
||||
if (isRight(streamResult)) {
|
||||
subscribeToStream(
|
||||
streamResult.right,
|
||||
(responseState) => {
|
||||
if (loading.value) {
|
||||
// Check exists because, loading can be set to false
|
||||
// when cancelled
|
||||
updateRESTResponse(responseState)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
subscribeToStream(
|
||||
runRESTRequest$(),
|
||||
(responseState) => {
|
||||
if (loading.value) {
|
||||
// Check exists because, loading can be set to false
|
||||
// when cancelled
|
||||
updateRESTResponse(responseState)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const cancelRequest = () => {
|
||||
@@ -428,6 +444,8 @@ const isCustomMethod = computed(() => {
|
||||
|
||||
const requestName = useRESTRequestName()
|
||||
|
||||
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
|
||||
|
||||
const windowInnerWidth = useWindowSize()
|
||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||
</script>
|
||||
|
||||
@@ -68,22 +68,11 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/youre_lost.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-32
|
||||
w-32
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center mb-2">
|
||||
<i class="opacity-75 pb-2 material-icons">cloud_off</i>
|
||||
<span class="text-center pb-2">
|
||||
{{ $t("error.network_fail") }}
|
||||
</span>
|
||||
<span class="text-center mb-4 max-w-sm">
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("helpers.network_fail") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</span>
|
||||
<span class="text-secondaryLight">
|
||||
{{
|
||||
` \xA0 — \xA0 ${
|
||||
` \xA0 — \xA0test ${
|
||||
result.status === "pass"
|
||||
? $t("test.passed")
|
||||
: $t("test.failed")
|
||||
@@ -78,11 +78,6 @@
|
||||
v-else
|
||||
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/validation.svg`"
|
||||
loading="lazy"
|
||||
class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
|
||||
/>
|
||||
<span class="text-center pb-2">
|
||||
{{ $t("empty.tests") }}
|
||||
</span>
|
||||
|
||||
@@ -17,16 +17,7 @@
|
||||
>
|
||||
<LensesHeadersRenderer :headers="response.headers" />
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
id="results"
|
||||
:label="$t('test.results')"
|
||||
:indicator="
|
||||
testResults &&
|
||||
(testResults.expectResults.length || testResults.tests.length)
|
||||
? true
|
||||
: false
|
||||
"
|
||||
>
|
||||
<SmartTab id="results" :label="$t('test.results')">
|
||||
<HttpTestResult />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
@@ -35,8 +26,6 @@
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { getSuitableLenses, getLensRenderers } from "~/helpers/lenses/lenses"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { restTestResults$ } from "~/newstore/RESTSession"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -46,12 +35,6 @@ export default defineComponent({
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
setup() {
|
||||
const testResults = useReadonlyStream(restTestResults$, null)
|
||||
return {
|
||||
testResults,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerLength() {
|
||||
if (!this.response || !this.response.headers) return 0
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
|
||||
:class="{
|
||||
'!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
|
||||
}"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||
>
|
||||
<Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<AppSection label="request">
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
@@ -33,7 +34,6 @@
|
||||
focus-visible:border-dividerDark
|
||||
"
|
||||
:placeholder="$t('mqtt.url')"
|
||||
:disabled="connectionState"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="connect"
|
||||
@@ -59,7 +59,8 @@
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="SIDEBAR"
|
||||
v-if="RIGHT_SIDEBAR"
|
||||
max-size="35"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto"
|
||||
@@ -158,9 +159,8 @@ export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
windowInnerWidth: useWindowSize(),
|
||||
SIDEBAR: useSetting("SIDEBAR"),
|
||||
RIGHT_SIDEBAR: useSetting("RIGHT_SIDEBAR"),
|
||||
COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
|
||||
SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,62 +1,20 @@
|
||||
<template>
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
|
||||
:class="{
|
||||
'!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
|
||||
}"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||
>
|
||||
<Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<AppSection label="request">
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
<div class="space-x-2 flex-1 inline-flex">
|
||||
<div class="flex flex-1">
|
||||
<label for="client-version">
|
||||
<tippy
|
||||
ref="versionOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
id="client-version"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
title="socket.io-client version"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
rounded-l
|
||||
cursor-pointer
|
||||
flex
|
||||
font-semibold
|
||||
text-secondaryDark
|
||||
py-2
|
||||
px-4
|
||||
w-26
|
||||
hover:border-dividerDark
|
||||
focus-visible:bg-transparent
|
||||
focus-visible:border-dividerDark
|
||||
"
|
||||
:value="`Client ${clientVersion}`"
|
||||
readonly
|
||||
:disabled="connectionState"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
v-for="(_, version) in socketIoClients"
|
||||
:key="`client-${version}`"
|
||||
:label="`Client ${version}`"
|
||||
@click.native="onSelectVersion(version)"
|
||||
/>
|
||||
</tippy>
|
||||
</label>
|
||||
<input
|
||||
id="socketio-url"
|
||||
v-model="url"
|
||||
@@ -68,6 +26,7 @@
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
rounded-l
|
||||
flex flex-1
|
||||
text-secondaryDark
|
||||
w-full
|
||||
@@ -78,7 +37,6 @@
|
||||
focus-visible:border-dividerDark
|
||||
"
|
||||
:placeholder="$t('socketio.url')"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<input
|
||||
@@ -98,7 +56,6 @@
|
||||
focus-visible:border-dividerDark
|
||||
"
|
||||
spellcheck="false"
|
||||
:disabled="connectionState"
|
||||
/>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
@@ -126,7 +83,8 @@
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="SIDEBAR"
|
||||
v-if="RIGHT_SIDEBAR"
|
||||
max-size="35"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto"
|
||||
@@ -209,39 +167,25 @@
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { Splitpanes, Pane } from "splitpanes"
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
// All Socket.IO client version imports
|
||||
import ClientV2 from "socket.io-client-v2"
|
||||
import { io as ClientV3 } from "socket.io-client-v3"
|
||||
import { io as ClientV4 } from "socket.io-client-v4"
|
||||
|
||||
import { io as Client } from "socket.io-client"
|
||||
import wildcard from "socketio-wildcard"
|
||||
import debounce from "lodash/debounce"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import useWindowSize from "~/helpers/utils/useWindowSize"
|
||||
|
||||
const socketIoClients = {
|
||||
v4: ClientV4,
|
||||
v3: ClientV3,
|
||||
v2: ClientV2,
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { Splitpanes, Pane },
|
||||
setup() {
|
||||
return {
|
||||
windowInnerWidth: useWindowSize(),
|
||||
SIDEBAR: useSetting("SIDEBAR"),
|
||||
RIGHT_SIDEBAR: useSetting("RIGHT_SIDEBAR"),
|
||||
COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
|
||||
SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
|
||||
socketIoClients,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// default version is set to v4
|
||||
clientVersion: "v4",
|
||||
url: "wss://hoppscotch-socketio.herokuapp.com",
|
||||
url: "wss://main-daxrc78qyb411dls-gtw.qovery.io",
|
||||
path: "/socket.io",
|
||||
isUrlValid: true,
|
||||
connectingState: false,
|
||||
@@ -263,10 +207,6 @@ export default defineComponent({
|
||||
url() {
|
||||
this.debouncer()
|
||||
},
|
||||
connectionState(connected) {
|
||||
if (connected) this.$refs.versionOptions.tippy().disable()
|
||||
else this.$refs.versionOptions.tippy().enable()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (process.browser) {
|
||||
@@ -310,8 +250,9 @@ export default defineComponent({
|
||||
if (!this.path) {
|
||||
this.path = "/socket.io"
|
||||
}
|
||||
const Client = socketIoClients[this.clientVersion]
|
||||
this.io = new Client(this.url, { path: this.path })
|
||||
this.io = new Client(this.url, {
|
||||
path: this.path,
|
||||
})
|
||||
// Add ability to listen to all events
|
||||
wildcard(Client.Manager)(this.io)
|
||||
this.io.on("connect", () => {
|
||||
@@ -421,10 +362,6 @@ export default defineComponent({
|
||||
this.communication.inputs = [""]
|
||||
}
|
||||
},
|
||||
onSelectVersion(version) {
|
||||
this.clientVersion = version
|
||||
this.$refs.versionOptions.tippy().hide()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
<div class="space-x-2 flex-1 inline-flex">
|
||||
@@ -24,11 +28,10 @@
|
||||
focus-visible:bg-transparent focus-visible:border-dividerDark
|
||||
"
|
||||
:placeholder="$t('sse.url')"
|
||||
:disabled="connectionSSEState"
|
||||
@keyup.enter="serverValid ? toggleSSEConnection() : null"
|
||||
/>
|
||||
<label
|
||||
for="event-type"
|
||||
for="url"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border-t border-b border-divider
|
||||
@@ -57,7 +60,6 @@
|
||||
focus-visible:bg-transparent focus-visible:border-dividerDark
|
||||
"
|
||||
spellcheck="false"
|
||||
:disabled="connectionSSEState"
|
||||
/>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
|
||||
:class="{
|
||||
'!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
|
||||
}"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||
>
|
||||
<Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<AppSection label="request">
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
@@ -34,7 +35,6 @@
|
||||
spellcheck="false"
|
||||
:class="{ error: !urlValid }"
|
||||
:placeholder="$t('websocket.url')"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
@@ -145,18 +145,7 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<i class="opacity-75 pb-2 material-icons">topic</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.protocols") }}
|
||||
</span>
|
||||
@@ -174,7 +163,8 @@
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="SIDEBAR"
|
||||
v-if="RIGHT_SIDEBAR"
|
||||
max-size="35"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto"
|
||||
@@ -229,16 +219,15 @@ export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
windowInnerWidth: useWindowSize(),
|
||||
SIDEBAR: useSetting("SIDEBAR"),
|
||||
RIGHT_SIDEBAR: useSetting("RIGHT_SIDEBAR"),
|
||||
COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
|
||||
SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
connectionState: false,
|
||||
connectingState: false,
|
||||
url: "wss://hoppscotch-websocket.herokuapp.com",
|
||||
url: "wss://echo.websocket.org",
|
||||
isUrlValid: true,
|
||||
socket: null,
|
||||
communication: {
|
||||
|
||||
@@ -231,7 +231,7 @@ export default defineComponent({
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-accentDark;
|
||||
@apply bg-accent;
|
||||
@apply text-accentContrast;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
class="env-input"
|
||||
:class="styles"
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
@keydown.enter.prevent="$emit('enter', $event)"
|
||||
@keyup="$emit('keyup', $event)"
|
||||
@click="$emit('click', $event)"
|
||||
@@ -481,7 +480,7 @@ export default defineComponent({
|
||||
@apply font-medium;
|
||||
|
||||
&:empty {
|
||||
@apply leading-loose;
|
||||
line-height: 1.9;
|
||||
|
||||
&::before {
|
||||
@apply text-secondary;
|
||||
|
||||
@@ -12,7 +12,6 @@ export default defineComponent({
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
info: { type: String, default: null },
|
||||
indicator: { type: Boolean, default: false },
|
||||
icon: { type: String, default: null },
|
||||
id: { type: String, default: null, required: true },
|
||||
selected: {
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
<span v-if="tab.info && tab.info !== 'null'" class="tab-info">
|
||||
{{ tab.info }}
|
||||
</span>
|
||||
<span
|
||||
v-if="tab.indicator"
|
||||
class="bg-accentLight h-1 w-1 ml-2 rounded-full"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
@@ -78,16 +74,6 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
const candidates = this.$children.filter(
|
||||
(child) => child.$options.name === "SmartTab"
|
||||
)
|
||||
|
||||
if (candidates.length !== this.tabs.length) {
|
||||
this.tabs = candidates
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.tabs = this.$children.filter(
|
||||
(child) => child.$options.name === "SmartTab"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('team.new').toString()" @close="hideModal">
|
||||
<SmartModal v-if="show" :title="$t('team.new')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<input
|
||||
@@ -19,12 +19,9 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<ButtonPrimary
|
||||
:label="$t('action.save').toString()"
|
||||
@click.native="addNewTeam"
|
||||
/>
|
||||
<ButtonPrimary :label="$t('action.save')" @click.native="addNewTeam" />
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel').toString()"
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
@@ -32,56 +29,54 @@
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { createTeam } from "~/helpers/backend/mutations/Team"
|
||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
|
||||
const {
|
||||
app: { i18n },
|
||||
$toast,
|
||||
} = useContext()
|
||||
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
|
||||
const addNewTeam = () =>
|
||||
pipe(
|
||||
TeamNameCodec.decode(name.value), // Perform decode (returns either)
|
||||
TE.fromEither, // Convert either to a task either
|
||||
TE.mapLeft(() => "invalid_name" as const), // Failure above is an invalid_name, give it an identifiable value
|
||||
TE.chainW(createTeam), // Create the team
|
||||
TE.match(
|
||||
(err) => {
|
||||
// err is of type "invalid_name" | GQLError<Err>
|
||||
if (err === "invalid_name") {
|
||||
$toast.error(t("team.name_length_insufficient").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
} else {
|
||||
// Handle GQL errors (use err obj)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// Success logic ?
|
||||
hideModal()
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewTeam() {
|
||||
// We save the user input in case of an error
|
||||
const name = this.name
|
||||
// We clear it early to give the UI a snappy feel
|
||||
this.name = ""
|
||||
if (!name) {
|
||||
this.$toast.error(this.$t("empty.team_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
if (name !== null && name.replace(/\s/g, "").length < 6) {
|
||||
this.$toast.error(this.$t("team.name_length_insufficient"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.createTeam(this.$apollo, name)
|
||||
.then(() => {
|
||||
this.hideModal()
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
// We restore the initial user input
|
||||
this.name = name
|
||||
})
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -17,40 +17,153 @@
|
||||
{{ $t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex pt-4 flex-1 justify-between items-center">
|
||||
<div class="flex flex-1 justify-between items-center">
|
||||
<label for="memberList" class="p-4">
|
||||
{{ $t("team.members") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
svg="user-plus"
|
||||
:label="$t('team.invite')"
|
||||
filled
|
||||
@click.native="
|
||||
() => {
|
||||
$emit('invite-team')
|
||||
}
|
||||
"
|
||||
svg="plus"
|
||||
:label="$t('add.new')"
|
||||
@click.native="addTeamMember"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="teamDetails.loading"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<SmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ $t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!teamDetails.loading &&
|
||||
E.isRight(teamDetails.data) &&
|
||||
teamDetails.data.right.team.teamMembers
|
||||
"
|
||||
class="divide-y divide-dividerLight border-divider border rounded"
|
||||
>
|
||||
<div class="divide-y divide-dividerLight border-divider border rounded">
|
||||
<div
|
||||
v-if="teamDetails.data.right.team.teamMembers === 0"
|
||||
v-for="(member, index) in members"
|
||||
:key="`member-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'param' + index"
|
||||
:value="member.user.email"
|
||||
readonly
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
:ref="`memberOptions-${index}`"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof member.role === 'string'
|
||||
? member.role
|
||||
: JSON.stringify(member.role)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="updateMemberRole(index, 'OWNER')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="updateMemberRole(index, 'EDITOR')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="updateMemberRole(index, 'VIEWER')"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeExistingTeamMember(member.user.uid)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in newMembers"
|
||||
:key="`new-member-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
v-model="member.key"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'member' + index"
|
||||
autofocus
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
:ref="`newMemberOptions-${index}`"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof member.value === 'string'
|
||||
? member.value
|
||||
: JSON.stringify(member.value)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="updateNewMemberRole(index, 'OWNER')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="updateNewMemberRole(index, 'EDITOR')"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="updateNewMemberRole(index, 'VIEWER')"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeTeamMember(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="members.length === 0 && newMembers.length === 0"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
@@ -59,121 +172,16 @@
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
my-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<SmartIcon class="opacity-75 pb-2" name="users" />
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.members") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
svg="user-plus"
|
||||
:label="$t('team.invite')"
|
||||
@click.native="
|
||||
() => {
|
||||
emit('invite-team')
|
||||
}
|
||||
"
|
||||
:label="$t('add.new')"
|
||||
filled
|
||||
@click.native="addTeamMember"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(member, index) in membersList"
|
||||
:key="`member-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'param' + index"
|
||||
:value="member.email"
|
||||
readonly
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="memberOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof member.role === 'string'
|
||||
? member.role
|
||||
: JSON.stringify(member.role)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="
|
||||
() => {
|
||||
updateMemberRole(member.userID, 'OWNER')
|
||||
memberOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="
|
||||
() => {
|
||||
updateMemberRole(member.userID, 'EDITOR')
|
||||
memberOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="
|
||||
() => {
|
||||
updateMemberRole(member.userID, 'VIEWER')
|
||||
memberOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeExistingTeamMember(member.userID)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamDetails.loading && E.isLeft(teamDetails.data)"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
{{ $t("error.something_went_wrong") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -189,230 +197,192 @@
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
toRef,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
GetTeamDocument,
|
||||
GetTeamQuery,
|
||||
GetTeamQueryVariables,
|
||||
TeamMemberAddedDocument,
|
||||
TeamMemberRemovedDocument,
|
||||
TeamMemberRole,
|
||||
TeamMemberUpdatedDocument,
|
||||
} from "~/helpers/backend/graphql"
|
||||
import {
|
||||
removeTeamMember,
|
||||
renameTeam,
|
||||
updateTeamMemberRole,
|
||||
} from "~/helpers/backend/mutations/Team"
|
||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
||||
import { useGQLQuery } from "~/helpers/backend/GQLClient"
|
||||
<script>
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
import TeamMemberAdapter from "~/helpers/teams/TeamMemberAdapter"
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const memberOptions = ref<any | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
editingTeam: {
|
||||
name: string
|
||||
}
|
||||
editingTeamID: string
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const name = toRef(props.editingTeam, "name")
|
||||
|
||||
watch(
|
||||
() => props.editingTeam.name,
|
||||
(newName: string) => {
|
||||
name.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.editingTeamID,
|
||||
(teamID: string) => {
|
||||
teamDetails.execute({ teamID })
|
||||
}
|
||||
)
|
||||
|
||||
const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({
|
||||
query: GetTeamDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
editingTeam: { type: Object, default: () => {} },
|
||||
editingteamID: { type: String, default: null },
|
||||
},
|
||||
defer: true,
|
||||
updateSubs: computed(() => {
|
||||
if (props.editingTeamID) {
|
||||
return [
|
||||
{
|
||||
key: 1,
|
||||
query: TeamMemberAddedDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
query: TeamMemberUpdatedDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
query: TeamMemberRemovedDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
},
|
||||
},
|
||||
]
|
||||
} else return []
|
||||
}),
|
||||
})
|
||||
|
||||
const roleUpdates = ref<
|
||||
{
|
||||
userID: string
|
||||
role: TeamMemberRole
|
||||
}[]
|
||||
>([])
|
||||
|
||||
watch(
|
||||
() => teamDetails,
|
||||
() => {
|
||||
if (teamDetails.loading) return
|
||||
|
||||
const data = teamDetails.data
|
||||
|
||||
if (E.isRight(data)) {
|
||||
const members = data.right.team?.teamMembers ?? []
|
||||
|
||||
// Remove deleted members
|
||||
roleUpdates.value = roleUpdates.value.filter(
|
||||
(update) =>
|
||||
members.findIndex((y) => y.user.uid === update.userID) !== -1
|
||||
)
|
||||
data() {
|
||||
return {
|
||||
rename: null,
|
||||
members: [],
|
||||
newMembers: [],
|
||||
membersAdapter: new TeamMemberAdapter(null),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const updateMemberRole = (userID: string, role: TeamMemberRole) => {
|
||||
const updateIndex = roleUpdates.value.findIndex(
|
||||
(item) => item.userID === userID
|
||||
)
|
||||
if (updateIndex !== -1) {
|
||||
// Role Update exists
|
||||
roleUpdates.value[updateIndex].role = role
|
||||
} else {
|
||||
// Role Update does not exist
|
||||
roleUpdates.value.push({
|
||||
userID,
|
||||
role,
|
||||
},
|
||||
computed: {
|
||||
editingTeamCopy() {
|
||||
return this.editingTeam
|
||||
},
|
||||
name: {
|
||||
get() {
|
||||
return this.editingTeam.name
|
||||
},
|
||||
set(name) {
|
||||
this.rename = name
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editingteamID(teamID) {
|
||||
this.membersAdapter.changeTeamID(teamID)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.membersAdapter.members$.subscribe((list) => {
|
||||
this.members = cloneDeep(list)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const membersList = computed(() => {
|
||||
if (teamDetails.loading) return []
|
||||
|
||||
const data = teamDetails.data
|
||||
|
||||
if (E.isLeft(data)) return []
|
||||
|
||||
if (E.isRight(data)) {
|
||||
const members = (data.right.team?.teamMembers ?? []).map((member) => {
|
||||
const updatedRole = roleUpdates.value.find(
|
||||
(update) => update.userID === member.user.uid
|
||||
)
|
||||
|
||||
return {
|
||||
userID: member.user.uid,
|
||||
email: member.user.email!,
|
||||
role: updatedRole?.role ?? member.role,
|
||||
}
|
||||
})
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const removeExistingTeamMember = async (userID: string) => {
|
||||
const removeTeamMemberResult = await removeTeamMember(
|
||||
userID,
|
||||
props.editingTeamID
|
||||
)()
|
||||
if (E.isLeft(removeTeamMemberResult)) {
|
||||
$toast.error(t("error.something_went_wrong"), {
|
||||
icon: "error",
|
||||
})
|
||||
} else {
|
||||
$toast.success(t("team.member_removed"), {
|
||||
icon: "done",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const saveTeam = async () => {
|
||||
if (name.value !== "") {
|
||||
if (TeamNameCodec.is(name.value)) {
|
||||
const updateTeamNameResult = await renameTeam(
|
||||
props.editingTeamID,
|
||||
name.value
|
||||
)()
|
||||
if (E.isLeft(updateTeamNameResult)) {
|
||||
$toast.error(t("error.something_went_wrong"), {
|
||||
icon: "error",
|
||||
},
|
||||
methods: {
|
||||
updateMemberRole(id, role) {
|
||||
this.members[id].role = role
|
||||
this.$refs[`memberOptions-${id}`][0].tippy().hide()
|
||||
},
|
||||
updateNewMemberRole(id, role) {
|
||||
this.newMembers[id].value = role
|
||||
this.$refs[`newMemberOptions-${id}`][0].tippy().hide()
|
||||
},
|
||||
addTeamMember() {
|
||||
const member = { key: "", value: "" }
|
||||
this.newMembers.push(member)
|
||||
},
|
||||
removeExistingTeamMember(userID) {
|
||||
teamUtils
|
||||
.removeTeamMember(this.$apollo, userID, this.editingteamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.member_removed"), {
|
||||
icon: "done",
|
||||
})
|
||||
this.hideModal()
|
||||
})
|
||||
} else {
|
||||
roleUpdates.value.forEach(async (update) => {
|
||||
const updateMemberRoleResult = await updateTeamMemberRole(
|
||||
update.userID,
|
||||
props.editingTeamID,
|
||||
update.role
|
||||
)()
|
||||
if (E.isLeft(updateMemberRoleResult)) {
|
||||
$toast.error(t("error.something_went_wrong"), {
|
||||
icon: "error",
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
},
|
||||
removeTeamMember(index) {
|
||||
this.newMembers.splice(index, 1)
|
||||
},
|
||||
validateEmail(emailID) {
|
||||
if (
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(
|
||||
emailID
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
saveTeam() {
|
||||
if (
|
||||
this.$data.rename !== null &&
|
||||
this.$data.rename.replace(/\s/g, "").length < 6
|
||||
) {
|
||||
this.$toast.error(this.$t("team.name_length_insufficient"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
let invalidEmail = false
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
if (!this.validateEmail(element.key)) {
|
||||
this.$toast.error(this.$t("team.invalid_email_format"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
invalidEmail = true
|
||||
}
|
||||
})
|
||||
if (invalidEmail) return
|
||||
let invalidPermission = false
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
if (!element.value) {
|
||||
this.$toast.error(this.$t("invalid_member_permission"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
invalidPermission = true
|
||||
}
|
||||
})
|
||||
if (invalidPermission) return
|
||||
this.$data.newMembers.forEach((element) => {
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.addTeamMemberByEmail(
|
||||
this.$apollo,
|
||||
element.value,
|
||||
element.key,
|
||||
this.editingteamID
|
||||
)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.saved"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(e, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
this.members.forEach((element) => {
|
||||
teamUtils
|
||||
.updateTeamMemberRole(
|
||||
this.$apollo,
|
||||
element.user.uid,
|
||||
element.role,
|
||||
this.editingteamID
|
||||
)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.member_role_updated"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(e, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
if (this.$data.rename !== null) {
|
||||
const newName =
|
||||
this.name === this.$data.rename ? this.name : this.$data.rename
|
||||
if (!/\S/.test(newName))
|
||||
return this.$toast.error(this.$t("empty.team_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
// Call to the graphql mutation
|
||||
if (this.name !== this.rename)
|
||||
teamUtils
|
||||
.renameTeam(this.$apollo, newName, this.editingteamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.saved"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
console.error(updateMemberRoleResult.left.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
hideModal()
|
||||
$toast.success(t("team.saved"), {
|
||||
icon: "done",
|
||||
})
|
||||
} else {
|
||||
return $toast.error(t("team.name_length_insufficient"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return $toast.error(t("empty.team_name"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.rename = null
|
||||
this.newMembers = []
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('team.invite')" @close="hideModal">
|
||||
<template #body>
|
||||
<div v-if="sendInvitesResult.length" class="flex flex-col px-4">
|
||||
<div class="flex flex-col max-w-md justify-center items-center">
|
||||
<SmartIcon class="h-6 text-accent w-6" name="users" />
|
||||
<h3 class="my-2 text-center text-lg">
|
||||
{{ $t("team.we_sent_invite_link") }}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
{{ $t("team.we_sent_invite_link_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
border border-dividerLight
|
||||
mt-8
|
||||
rounded
|
||||
flex-col
|
||||
space-y-6
|
||||
p-4
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(invitee, index) in sendInvitesResult"
|
||||
:key="`invitee-${index}`"
|
||||
>
|
||||
<p class="flex items-center">
|
||||
<i
|
||||
class="material-icons mr-4"
|
||||
:class="
|
||||
invitee.status === 'error' ? 'text-red-500' : 'text-green-500'
|
||||
"
|
||||
>
|
||||
{{
|
||||
invitee.status === "error"
|
||||
? "error_outline"
|
||||
: "mark_email_read"
|
||||
}}
|
||||
</i>
|
||||
<span class="truncate">{{ invitee.email }}</span>
|
||||
</p>
|
||||
<p v-if="invitee.status === 'error'" class="ml-8 text-red-500 mt-2">
|
||||
{{ getErrorMessage(invitee.error) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="sendingInvites"
|
||||
class="flex p-4 items-center justify-center"
|
||||
>
|
||||
<SmartSpinner />
|
||||
</div>
|
||||
<div v-else class="flex flex-col px-2">
|
||||
<div class="flex flex-1 justify-between items-center">
|
||||
<label for="memberList" class="pb-4 px-4">
|
||||
{{ $t("team.pending_invites") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="divide-y divide-dividerLight border-divider border rounded">
|
||||
<div
|
||||
v-if="pendingInvites.loading"
|
||||
class="flex p-4 items-center justify-center"
|
||||
>
|
||||
<SmartSpinner />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="!pendingInvites.loading && E.isRight(pendingInvites.data)"
|
||||
>
|
||||
<div
|
||||
v-for="(invitee, index) in pendingInvites.data.right.team
|
||||
.teamInvitations"
|
||||
:key="`invitee-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
v-if="invitee"
|
||||
class="
|
||||
bg-transparent
|
||||
flex flex-1
|
||||
text-secondaryLight
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="`${$t('team.email')}`"
|
||||
:name="'param' + index"
|
||||
:value="invitee.inviteeEmail"
|
||||
readonly
|
||||
/>
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
flex flex-1
|
||||
text-secondaryLight
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="`${$t('team.permissions')}`"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof invitee.inviteeRole === 'string'
|
||||
? invitee.inviteeRole
|
||||
: JSON.stringify(invitee.inviteeRole)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeInvitee(invitee.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
E.isRight(pendingInvites.data) &&
|
||||
pendingInvites.data.right.team.teamInvitations.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.pending_invites") }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
||||
class="flex flex-col p-4 items-center"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
{{ $t("error.something_went_wrong") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-4 flex-1 justify-between items-center">
|
||||
<label for="memberList" class="p-4">
|
||||
{{ $t("team.invite_tooltip") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
svg="plus"
|
||||
:label="$t('add.new')"
|
||||
filled
|
||||
@click.native="addNewInvitee"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y divide-dividerLight border-divider border rounded">
|
||||
<div
|
||||
v-for="(invitee, index) in newInvites"
|
||||
:key="`new-invitee-${index}`"
|
||||
class="divide-x divide-dividerLight flex"
|
||||
>
|
||||
<input
|
||||
v-model="invitee.key"
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('team.email')"
|
||||
:name="'invitee' + index"
|
||||
autofocus
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="newInviteeOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<input
|
||||
class="
|
||||
bg-transparent
|
||||
cursor-pointer
|
||||
flex flex-1
|
||||
py-2
|
||||
px-4
|
||||
"
|
||||
:placeholder="$t('team.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="
|
||||
typeof invitee.value === 'string'
|
||||
? invitee.value
|
||||
: JSON.stringify(invitee.value)
|
||||
"
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<SmartItem
|
||||
label="OWNER"
|
||||
@click.native="
|
||||
() => {
|
||||
updateNewInviteeRole(index, 'OWNER')
|
||||
newInviteeOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="EDITOR"
|
||||
@click.native="
|
||||
() => {
|
||||
updateNewInviteeRole(index, 'EDITOR')
|
||||
newInviteeOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="VIEWER"
|
||||
@click.native="
|
||||
() => {
|
||||
updateNewInviteeRole(index, 'VIEWER')
|
||||
newInviteeOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeNewInvitee(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="newInvites.length === 0"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-4
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.invites") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="$t('add.new')"
|
||||
filled
|
||||
@click.native="addNewInvitee"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="newInvites.length"
|
||||
class="
|
||||
px-4
|
||||
mt-4
|
||||
py-4
|
||||
rounded
|
||||
border border-dividerLight
|
||||
bg-primaryLight
|
||||
"
|
||||
>
|
||||
<span class="pb-2 flex items-center font-semibold">
|
||||
<i class="text-secondaryLight mr-2 material-icons">help_outline</i>
|
||||
{{ $t("profile.roles") }}
|
||||
</span>
|
||||
<p>
|
||||
<span class="text-secondaryLight">
|
||||
{{ $t("profile.roles_description") }}
|
||||
</span>
|
||||
</p>
|
||||
<ul class="mt-4 space-y-4">
|
||||
<li class="flex">
|
||||
<span
|
||||
class="
|
||||
font-semibold
|
||||
text-secondaryDark
|
||||
uppercase
|
||||
truncate
|
||||
max-w-16
|
||||
w-1/4
|
||||
"
|
||||
>
|
||||
{{ $t("profile.owner") }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
{{ $t("profile.owner_description") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="
|
||||
font-semibold
|
||||
text-secondaryDark
|
||||
uppercase
|
||||
truncate
|
||||
max-w-16
|
||||
w-1/4
|
||||
"
|
||||
>
|
||||
{{ $t("profile.editor") }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
{{ $t("profile.editor_description") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="
|
||||
font-semibold
|
||||
text-secondaryDark
|
||||
uppercase
|
||||
truncate
|
||||
max-w-16
|
||||
w-1/4
|
||||
"
|
||||
>
|
||||
{{ $t("profile.viewer") }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
{{ $t("profile.viewer_description") }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<p
|
||||
v-if="sendInvitesResult.length"
|
||||
class="flex flex-1 text-secondaryLight justify-between"
|
||||
>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="`← \xA0 ${$t('team.invite_more')}`"
|
||||
@click.native="
|
||||
() => {
|
||||
sendInvitesResult = []
|
||||
newInvites = [
|
||||
{
|
||||
key: '',
|
||||
value: 'VIEWRER',
|
||||
},
|
||||
]
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="`${$t('action.dismiss')}`"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</p>
|
||||
<span v-else>
|
||||
<ButtonPrimary :label="$t('team.invite')" @click.native="sendInvites" />
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
watch,
|
||||
ref,
|
||||
reactive,
|
||||
useContext,
|
||||
computed,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import * as T from "fp-ts/Task"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import { Email, EmailCodec } from "../../helpers/backend/types/Email"
|
||||
import {
|
||||
TeamInvitationAddedDocument,
|
||||
TeamInvitationRemovedDocument,
|
||||
TeamMemberRole,
|
||||
} from "../../helpers/backend/graphql"
|
||||
import {
|
||||
createTeamInvitation,
|
||||
CreateTeamInvitationErrors,
|
||||
revokeTeamInvitation,
|
||||
} from "../../helpers/backend/mutations/TeamInvitation"
|
||||
import { GQLError, useGQLQuery } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
GetPendingInvitesDocument,
|
||||
GetPendingInvitesQuery,
|
||||
GetPendingInvitesQueryVariables,
|
||||
} from "~/helpers/backend/graphql"
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const newInviteeOptions = ref<any | null>(null)
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
editingTeamID: { type: String, default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const pendingInvites = useGQLQuery<
|
||||
GetPendingInvitesQuery,
|
||||
GetPendingInvitesQueryVariables,
|
||||
""
|
||||
>({
|
||||
query: GetPendingInvitesDocument,
|
||||
variables: reactive({
|
||||
teamID: props.editingTeamID,
|
||||
}),
|
||||
updateSubs: computed(() =>
|
||||
!props.editingTeamID
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: 4,
|
||||
query: TeamInvitationAddedDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
query: TeamInvitationRemovedDocument,
|
||||
variables: {
|
||||
teamID: props.editingTeamID,
|
||||
},
|
||||
},
|
||||
]
|
||||
),
|
||||
defer: true,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.editingTeamID,
|
||||
() => {
|
||||
if (props.editingTeamID) {
|
||||
pendingInvites.execute({
|
||||
teamID: props.editingTeamID,
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const removeInvitee = async (id: string) => {
|
||||
const result = await revokeTeamInvitation(id)()
|
||||
if (E.isLeft(result)) {
|
||||
$toast.error(`${t("error.something_went_wrong")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
} else {
|
||||
$toast.success(`${t("team.member_removed")}`, {
|
||||
icon: "person",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const newInvites = ref<Array<{ key: string; value: TeamMemberRole }>>([
|
||||
{
|
||||
key: "",
|
||||
value: TeamMemberRole.Viewer,
|
||||
},
|
||||
])
|
||||
|
||||
const addNewInvitee = () => {
|
||||
newInvites.value.push({
|
||||
key: "",
|
||||
value: TeamMemberRole.Viewer,
|
||||
})
|
||||
}
|
||||
|
||||
const updateNewInviteeRole = (index: number, role: TeamMemberRole) => {
|
||||
newInvites.value[index].value = role
|
||||
}
|
||||
|
||||
const removeNewInvitee = (id: number) => {
|
||||
newInvites.value.splice(id, 1)
|
||||
}
|
||||
|
||||
type SendInvitesErrorType =
|
||||
| {
|
||||
email: Email
|
||||
status: "error"
|
||||
error: GQLError<CreateTeamInvitationErrors>
|
||||
}
|
||||
| {
|
||||
email: Email
|
||||
status: "success"
|
||||
}
|
||||
|
||||
const sendInvitesResult = ref<Array<SendInvitesErrorType>>([])
|
||||
|
||||
const sendingInvites = ref<boolean>(false)
|
||||
|
||||
const sendInvites = async () => {
|
||||
const validationResult = pipe(
|
||||
newInvites.value,
|
||||
O.fromPredicate(
|
||||
(invites): invites is Array<{ key: Email; value: TeamMemberRole }> =>
|
||||
pipe(
|
||||
invites,
|
||||
A.every((invitee) => EmailCodec.is(invitee.key))
|
||||
)
|
||||
),
|
||||
O.map(
|
||||
A.map((invitee) =>
|
||||
createTeamInvitation(invitee.key, invitee.value, props.editingTeamID)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (O.isNone(validationResult)) {
|
||||
// Error handling for no validation
|
||||
$toast.error(`${t("error.incorrect_email")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sendingInvites.value = true
|
||||
|
||||
sendInvitesResult.value = await pipe(
|
||||
A.sequence(T.task)(validationResult.value),
|
||||
T.chain(
|
||||
flow(
|
||||
A.mapWithIndex((i, el) =>
|
||||
pipe(
|
||||
el,
|
||||
E.foldW(
|
||||
(err) => ({
|
||||
status: "error" as const,
|
||||
email: newInvites.value[i].key as Email,
|
||||
error: err,
|
||||
}),
|
||||
() => ({
|
||||
status: "success" as const,
|
||||
email: newInvites.value[i].key as Email,
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
T.of
|
||||
)
|
||||
)
|
||||
)()
|
||||
|
||||
sendingInvites.value = false
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: SendInvitesErrorType) => {
|
||||
if (error.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (error.error) {
|
||||
case "team/invalid_id":
|
||||
return t("team.invalid_id")
|
||||
case "team/member_not_found":
|
||||
return t("team.member_not_found")
|
||||
case "team_invite/already_member":
|
||||
return t("team.already_member")
|
||||
case "team_invite/member_has_invite":
|
||||
return t("team.member_has_invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
sendingInvites.value = false
|
||||
sendInvitesResult.value = []
|
||||
newInvites.value = [
|
||||
{
|
||||
key: "",
|
||||
value: TeamMemberRole.Viewer,
|
||||
},
|
||||
]
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
:title="$t('team.select_a_team')"
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<Teams :modal="true" />
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,200 +1,117 @@
|
||||
<template>
|
||||
<div class="border border-divider rounded flex flex-col flex-1">
|
||||
<div
|
||||
class="flex flex-1 items-start"
|
||||
:class="
|
||||
compact
|
||||
? team.myRole === 'OWNER'
|
||||
? 'cursor-pointer hover:bg-primaryDark transition hover:border-dividerDark focus-visible:border-dividerDark'
|
||||
: 'cursor-not-allowed bg-primaryLight'
|
||||
: ''
|
||||
"
|
||||
@click="
|
||||
compact
|
||||
? team.myRole === 'OWNER'
|
||||
? $emit('invite-team')
|
||||
: noPermission()
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="border border-dividerLight rounded flex flex-1 items-end">
|
||||
<div class="flex flex-1 items-start">
|
||||
<div class="p-4">
|
||||
<label
|
||||
class="font-semibold text-secondaryDark"
|
||||
:class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }"
|
||||
class="cursor-pointer transition hover:text-secondaryDark"
|
||||
@click="team.myRole === 'OWNER' ? $emit('edit-team') : ''"
|
||||
>
|
||||
{{ team.name || $t("state.nothing_found") }}
|
||||
</label>
|
||||
<div class="flex -space-x-1 mt-2 overflow-hidden">
|
||||
<img
|
||||
v-for="(member, index) in team.teamMembers"
|
||||
v-for="(member, index) in team.members"
|
||||
:key="`member-${index}`"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="member.user.displayName"
|
||||
:src="member.user.photoURL || undefined"
|
||||
:src="member.user.photoURL"
|
||||
:alt="member.user.displayName"
|
||||
class="rounded-full h-5 ring-primary ring-2 w-5 inline-block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!compact" class="flex flex-shrink-0 items-end justify-between">
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
<span>
|
||||
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.more')"
|
||||
svg="more-vertical"
|
||||
/>
|
||||
</template>
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="edit"
|
||||
class="rounded-none"
|
||||
:label="$t('action.edit').toString()"
|
||||
:label="$t('action.edit')"
|
||||
@click.native="
|
||||
() => {
|
||||
$emit('edit-team')
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="user-plus"
|
||||
class="rounded-none"
|
||||
:label="$t('team.invite')"
|
||||
svg="trash-2"
|
||||
color="red"
|
||||
:label="$t('action.delete')"
|
||||
@click.native="
|
||||
() => {
|
||||
emit('invite-team')
|
||||
deleteTeam()
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
|
||||
<template #trigger>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.more')"
|
||||
svg="more-vertical"
|
||||
/>
|
||||
</template>
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="edit"
|
||||
:label="$t('action.edit').toString()"
|
||||
@click.native="
|
||||
() => {
|
||||
$emit('edit-team')
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="team.myRole === 'OWNER'"
|
||||
svg="trash-2"
|
||||
color="red"
|
||||
:label="$t('action.delete').toString()"
|
||||
@click.native="
|
||||
() => {
|
||||
deleteTeam()
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
|
||||
svg="trash"
|
||||
:label="$t('team.exit').toString()"
|
||||
@click.native="
|
||||
() => {
|
||||
exitTeam()
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
<SmartItem
|
||||
v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
|
||||
svg="trash"
|
||||
:label="$t('team.exit')"
|
||||
@click.native="
|
||||
() => {
|
||||
exitTeam()
|
||||
$refs.options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useContext } from "@nuxtjs/composition-api"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { TeamMemberRole } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
deleteTeam as backendDeleteTeam,
|
||||
leaveTeam,
|
||||
} from "~/helpers/backend/mutations/Team"
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
team: {
|
||||
name: string
|
||||
myRole: TeamMemberRole
|
||||
ownersCount: number
|
||||
teamMembers: Array<{
|
||||
user: {
|
||||
displayName: string
|
||||
photoURL: string | null
|
||||
}
|
||||
}>
|
||||
}
|
||||
teamID: string
|
||||
compact: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "edit-team"): void
|
||||
}>()
|
||||
|
||||
const {
|
||||
app: { i18n },
|
||||
$toast,
|
||||
} = useContext()
|
||||
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const deleteTeam = () => {
|
||||
if (!confirm(t("confirm.remove_team").toString())) return
|
||||
|
||||
pipe(
|
||||
backendDeleteTeam(props.teamID),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// TODO: Better errors ? We know the possible errors now
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
export default defineComponent({
|
||||
props: {
|
||||
team: { type: Object, default: () => {} },
|
||||
teamID: { type: String, default: null },
|
||||
},
|
||||
methods: {
|
||||
deleteTeam() {
|
||||
if (!confirm(this.$t("confirm.remove_team"))) return
|
||||
// Call to the graphql mutation
|
||||
teamUtils
|
||||
.deleteTeam(this.$apollo, this.teamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.deleted"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
console.error(err)
|
||||
},
|
||||
() => {
|
||||
$toast.success(t("team.deleted").toString(), {
|
||||
icon: "done",
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
)
|
||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||
}
|
||||
|
||||
const exitTeam = () => {
|
||||
if (!confirm("Are you sure you want to exit this team?")) return
|
||||
|
||||
pipe(
|
||||
leaveTeam(props.teamID),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// TODO: Better errors ?
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
},
|
||||
exitTeam() {
|
||||
if (!confirm("Are you sure you want to exit this team?")) return
|
||||
teamUtils
|
||||
.exitTeam(this.$apollo, this.teamID)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t("team.left"), {
|
||||
icon: "done",
|
||||
})
|
||||
})
|
||||
console.error(err)
|
||||
},
|
||||
() => {
|
||||
$toast.success(t("team.left").toString(), {
|
||||
icon: "done",
|
||||
.catch((e) => {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
)
|
||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||
}
|
||||
|
||||
const noPermission = () => {
|
||||
$toast.error(t("profile.no_permission").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,136 +1,93 @@
|
||||
<template>
|
||||
<AppSection label="teams">
|
||||
<div class="space-y-4 p-4">
|
||||
<h4 class="text-secondaryDark">
|
||||
{{ $t("team.title") }}
|
||||
</h4>
|
||||
<div class="mt-1 text-secondaryLight">
|
||||
<SmartAnchor
|
||||
:label="`${$t('team.join_beta')}`"
|
||||
to="https://hoppscotch.io/beta"
|
||||
blank
|
||||
class="link"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4 mt-4">
|
||||
<ButtonSecondary
|
||||
:label="`${$t('team.create_new')}`"
|
||||
outline
|
||||
@click.native="displayModalAdd(true)"
|
||||
/>
|
||||
<div
|
||||
v-if="myTeams.loading"
|
||||
v-if="myTeamsLoading"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<SmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ $t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!myTeams.loading &&
|
||||
E.isRight(myTeams.data) &&
|
||||
myTeams.data.right.myTeams.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
v-if="!myTeamsLoading && myTeams.myTeams.length === 0"
|
||||
class="flex items-center"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="
|
||||
flex-col
|
||||
mb-8
|
||||
object-contain object-center
|
||||
h-16
|
||||
w-16
|
||||
inline-flex
|
||||
"
|
||||
/>
|
||||
<span class="text-center mb-4">
|
||||
{{ $t("empty.teams") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="`${$t('team.create_new')}`"
|
||||
filled
|
||||
@click.native="displayModalAdd(true)"
|
||||
/>
|
||||
<i class="mr-4 material-icons">help_outline</i>
|
||||
{{ $t("empty.teams") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!myTeams.loading && E.isRight(myTeams.data)"
|
||||
class="grid gap-4"
|
||||
:class="
|
||||
modal ? 'grid-cols-1' : 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
|
||||
"
|
||||
v-else-if="!myTeamsLoading && !isApolloError(myTeams)"
|
||||
class="grid gap-4 sm:grid-cols-2 md:grid-cols-3"
|
||||
>
|
||||
<TeamsTeam
|
||||
v-for="(team, index) in myTeams.data.right.myTeams"
|
||||
v-for="(team, index) in myTeams.myTeams"
|
||||
:key="`team-${String(index)}`"
|
||||
:team-i-d="team.id"
|
||||
:team="team"
|
||||
:compact="modal"
|
||||
@edit-team="editTeam(team, team.id)"
|
||||
@invite-team="inviteTeam(team, team.id)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!myTeams.loading && E.isLeft(myTeams.data)"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
{{ $t("error.something_went_wrong") }}
|
||||
</div>
|
||||
</div>
|
||||
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
|
||||
<!-- ¯\_(ツ)_/¯ -->
|
||||
<TeamsEdit
|
||||
v-if="
|
||||
!myTeams.loading &&
|
||||
E.isRight(myTeams.data) &&
|
||||
myTeams.data.right.myTeams.length > 0
|
||||
"
|
||||
:team="myTeams.data.right.myTeams[0]"
|
||||
v-if="!myTeamsLoading && myTeams.myTeams.length > 0"
|
||||
:team="myTeams.myTeams[0]"
|
||||
:show="showModalEdit"
|
||||
:editing-team="editingTeam"
|
||||
:editing-team-i-d="editingTeamID"
|
||||
:editingteam-i-d="editingTeamID"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
@invite-team="inviteTeam(editingTeam, editingTeamID)"
|
||||
/>
|
||||
<TeamsInvite
|
||||
v-if="
|
||||
!myTeams.loading &&
|
||||
E.isRight(myTeams.data) &&
|
||||
myTeams.data.right.myTeams.length > 0
|
||||
"
|
||||
:team="myTeams.data.right.myTeams[0]"
|
||||
:show="showModalInvite"
|
||||
:editing-team="editingTeam"
|
||||
:editing-team-i-d="editingTeamID"
|
||||
@hide-modal="displayModalInvite(false)"
|
||||
/>
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gql } from "@apollo/client/core"
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useGQLQuery } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
MyTeamsDocument,
|
||||
MyTeamsQuery,
|
||||
MyTeamsQueryVariables,
|
||||
} from "~/helpers/backend/graphql"
|
||||
import { MyTeamsQueryError } from "~/helpers/backend/QueryErrors"
|
||||
|
||||
defineProps<{
|
||||
modal: boolean
|
||||
}>()
|
||||
import { useGQLQuery, isApolloError } from "~/helpers/apollo"
|
||||
|
||||
const showModalAdd = ref(false)
|
||||
const showModalEdit = ref(false)
|
||||
const showModalInvite = ref(false)
|
||||
const editingTeam = ref<any>({}) // TODO: Check this out
|
||||
const editingTeamID = ref<any>("")
|
||||
|
||||
const myTeams = useGQLQuery<
|
||||
MyTeamsQuery,
|
||||
MyTeamsQueryVariables,
|
||||
MyTeamsQueryError
|
||||
>({
|
||||
query: MyTeamsDocument,
|
||||
pollDuration: 5000,
|
||||
const { loading: myTeamsLoading, data: myTeams } = useGQLQuery({
|
||||
query: gql`
|
||||
query GetMyTeams {
|
||||
myTeams {
|
||||
id
|
||||
name
|
||||
myRole
|
||||
ownersCount
|
||||
members {
|
||||
user {
|
||||
photoURL
|
||||
displayName
|
||||
email
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
pollInterval: 10000,
|
||||
})
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
@@ -139,21 +96,17 @@ const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
|
||||
const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
showModalEdit.value = shouldDisplay
|
||||
}
|
||||
|
||||
const displayModalInvite = (shouldDisplay: boolean) => {
|
||||
showModalInvite.value = shouldDisplay
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const editTeam = (team: any, teamID: any) => {
|
||||
editingTeam.value = team
|
||||
editingTeamID.value = teamID
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
const inviteTeam = (team: any, teamID: any) => {
|
||||
editingTeam.value = team
|
||||
editingTeamID.value = teamID
|
||||
displayModalInvite(true)
|
||||
const resetSelectedData = () => {
|
||||
editingTeam.value = undefined
|
||||
editingTeamID.value = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
overwrite: true
|
||||
schema: https://api.hoppscotch.io/graphql
|
||||
generates:
|
||||
helpers/backend/graphql.ts:
|
||||
documents: "**/*.graphql"
|
||||
plugins:
|
||||
- add:
|
||||
content: // Auto-generated file (DO NOT EDIT!!!), refer gql-codegen.yml
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typed-document-node
|
||||
- typescript-urql-graphcache
|
||||
|
||||
helpers/backend/backend-schema.json:
|
||||
plugins:
|
||||
- urql-introspection
|
||||
@@ -109,9 +109,5 @@ function parseV1ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
resolvedReq.endpoint = urlParams.endpoint
|
||||
}
|
||||
|
||||
if (urlParams.body && typeof urlParams.body === "string") {
|
||||
resolvedReq.body = JSON.parse(urlParams.body)
|
||||
}
|
||||
|
||||
return resolvedReq
|
||||
}
|
||||
|
||||
@@ -1,86 +1,155 @@
|
||||
import { Observable } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import { chain, right, TaskEither } from "fp-ts/lib/TaskEither"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import { runTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||
import { isRight } from "fp-ts/lib/Either"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import getEnvironmentVariablesFromScript from "./preRequest"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import runTestScriptWithVariables, {
|
||||
transformResponseForTesting,
|
||||
} from "./postwomanTesting"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
|
||||
|
||||
const getTestableBody = (res: HoppRESTResponse & { type: "success" }) => {
|
||||
const contentTypeHeader = res.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
/**
|
||||
* Runs a REST network request along with all the
|
||||
* other side processes (like running test scripts)
|
||||
*/
|
||||
export function runRESTRequest$(): Observable<HoppRESTResponse> {
|
||||
const envs = getEnvironmentVariablesFromScript(
|
||||
getRESTRequest().preRequestScript
|
||||
)
|
||||
|
||||
const rawBody = new TextDecoder("utf-8").decode(res.body)
|
||||
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
|
||||
name: "Env",
|
||||
variables: Object.keys(envs).map((key) => {
|
||||
return {
|
||||
key,
|
||||
value: envs[key],
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
if (!contentTypeHeader || !isJSONContentType(contentTypeHeader.value))
|
||||
return rawBody
|
||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||
|
||||
return JSON.parse(rawBody)
|
||||
}
|
||||
|
||||
export const runRESTRequest$ = (): TaskEither<
|
||||
string,
|
||||
Observable<HoppRESTResponse>
|
||||
> =>
|
||||
pipe(
|
||||
getFinalEnvsFromPreRequest(
|
||||
getRESTRequest().preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
),
|
||||
chain((envs) => {
|
||||
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
|
||||
name: "Env",
|
||||
variables: envs,
|
||||
})
|
||||
|
||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||
|
||||
// Run Test Script when request ran successfully
|
||||
const subscription = stream
|
||||
.pipe(filter((res) => res.type === "success"))
|
||||
.subscribe(async (res) => {
|
||||
if (res.type === "success") {
|
||||
const runResult = await runTestScript(res.req.testScript, {
|
||||
status: res.statusCode,
|
||||
body: getTestableBody(res),
|
||||
headers: res.headers,
|
||||
})()
|
||||
|
||||
// TODO: Handle script executation fails (isLeft)
|
||||
if (isRight(runResult)) {
|
||||
setRESTTestResults(translateToSandboxTestResults(runResult.right))
|
||||
// Run Test Script when request ran successfully
|
||||
const subscription = stream
|
||||
.pipe(filter((res) => res.type === "success"))
|
||||
.subscribe((res) => {
|
||||
const testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
} = runTestScriptWithVariables(effectiveRequest.testScript, {
|
||||
response: transformResponseForTesting(res),
|
||||
}) as any
|
||||
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
setRESTTestResults(translateToNewTestResults(testReport))
|
||||
|
||||
return right(stream)
|
||||
subscription.unsubscribe()
|
||||
})
|
||||
)
|
||||
|
||||
function translateToSandboxTestResults(
|
||||
testDesc: TestDescriptor
|
||||
): HoppTestResult {
|
||||
const translateChildTests = (child: TestDescriptor): HoppTestData => {
|
||||
return {
|
||||
description: child.descriptor,
|
||||
expectResults: child.expectResults,
|
||||
tests: child.children.map(translateChildTests),
|
||||
return stream
|
||||
}
|
||||
|
||||
function isTestPass(x: any): x is {
|
||||
result: "PASS"
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "PASS"
|
||||
}
|
||||
|
||||
function isTestFail(x: any): x is {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "FAIL"
|
||||
}
|
||||
|
||||
function isStartBlock(
|
||||
x: any
|
||||
): x is { startBlock: string; styles: { icon: ""; class: "" } } {
|
||||
return x.startBlock !== undefined
|
||||
}
|
||||
|
||||
function isEndBlock(
|
||||
x: any
|
||||
): x is { endBlock: true; styles: { icon: ""; class: "" } } {
|
||||
return x.endBlock !== undefined
|
||||
}
|
||||
|
||||
function translateToNewTestResults(testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
}): HoppTestResult {
|
||||
// Build a stack of test data which we eventually build up based on the results
|
||||
const testsStack: HoppTestData[] = [
|
||||
{
|
||||
description: "root",
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
},
|
||||
]
|
||||
|
||||
testReport.testResults.forEach((result) => {
|
||||
// This is a test block start, push an empty test to the stack
|
||||
if (isStartBlock(result)) {
|
||||
testsStack.push({
|
||||
description: result.startBlock,
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
})
|
||||
} else if (isEndBlock(result)) {
|
||||
// End of the block, pop the stack and add it as a child to the current stack top
|
||||
const testData = testsStack.pop()!
|
||||
testsStack[testsStack.length - 1].tests.push(testData)
|
||||
} else if (isTestPass(result)) {
|
||||
// A normal PASS expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: result.message,
|
||||
})
|
||||
} else if (isTestFail(result)) {
|
||||
// A normal FAIL expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: result.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// We should end up with only the root stack entry
|
||||
if (testsStack.length !== 1) throw new Error("Invalid test result structure")
|
||||
|
||||
return {
|
||||
expectResults: testDesc.expectResults,
|
||||
tests: testDesc.children.map(translateChildTests),
|
||||
expectResults: testsStack[0].expectResults,
|
||||
tests: testsStack[0].tests,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export type HoppAction =
|
||||
| "navigation.jump.realtime" // Jump to realtime page
|
||||
| "navigation.jump.documentation" // Jump to documentation page
|
||||
| "navigation.jump.settings" // Jump to settings page
|
||||
| "navigation.jump.back" // Jump to previous page
|
||||
| "navigation.jump.forward" // Jump to next page
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
Ref,
|
||||
unref,
|
||||
watchEffect,
|
||||
watchSyncEffect,
|
||||
WatchStopHandle,
|
||||
set,
|
||||
isRef,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import {
|
||||
createClient,
|
||||
TypedDocumentNode,
|
||||
OperationResult,
|
||||
dedupExchange,
|
||||
OperationContext,
|
||||
fetchExchange,
|
||||
makeOperation,
|
||||
GraphQLRequest,
|
||||
createRequest,
|
||||
subscriptionExchange,
|
||||
} from "@urql/core"
|
||||
import { authExchange } from "@urql/exchange-auth"
|
||||
import { offlineExchange } from "@urql/exchange-graphcache"
|
||||
import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
|
||||
import { devtoolsExchange } from "@urql/devtools"
|
||||
import { SubscriptionClient } from "subscriptions-transport-ws"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe, constVoid } from "fp-ts/function"
|
||||
import { Source, subscribe, pipe as wonkaPipe, onEnd } from "wonka"
|
||||
import { keyDefs } from "./caching/keys"
|
||||
import { optimisticDefs } from "./caching/optimistic"
|
||||
import { updatesDef } from "./caching/updates"
|
||||
import { resolversDef } from "./caching/resolvers"
|
||||
import schema from "./backend-schema.json"
|
||||
import {
|
||||
authIdToken$,
|
||||
getAuthIDToken,
|
||||
probableUser$,
|
||||
waitProbableLoginToConfirm,
|
||||
} from "~/helpers/fb/auth"
|
||||
|
||||
const BACKEND_GQL_URL =
|
||||
process.env.context === "production"
|
||||
? "https://api.hoppscotch.io/graphql"
|
||||
: "https://api.hoppscotch.io/graphql"
|
||||
|
||||
const storage = makeDefaultStorage({
|
||||
idbName: "hoppcache-v1",
|
||||
maxAge: 7,
|
||||
})
|
||||
|
||||
const subscriptionClient = new SubscriptionClient(
|
||||
process.env.context === "production"
|
||||
? "wss://api.hoppscotch.io/graphql"
|
||||
: "wss://api.hoppscotch.io/graphql",
|
||||
{
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authIdToken$.value}`,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
authIdToken$.subscribe(() => {
|
||||
subscriptionClient.client.close()
|
||||
})
|
||||
|
||||
const createHoppClient = () =>
|
||||
createClient({
|
||||
url: BACKEND_GQL_URL,
|
||||
exchanges: [
|
||||
devtoolsExchange,
|
||||
dedupExchange,
|
||||
offlineExchange({
|
||||
schema: schema as any,
|
||||
keys: keyDefs,
|
||||
optimistic: optimisticDefs,
|
||||
updates: updatesDef,
|
||||
resolvers: resolversDef,
|
||||
storage,
|
||||
}),
|
||||
authExchange({
|
||||
addAuthToOperation({ authState, operation }) {
|
||||
if (!authState || !authState.authToken) {
|
||||
return operation
|
||||
}
|
||||
|
||||
const fetchOptions =
|
||||
typeof operation.context.fetchOptions === "function"
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {}
|
||||
|
||||
return makeOperation(operation.kind, operation, {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
willAuthError({ authState }) {
|
||||
return !authState || !authState.authToken
|
||||
},
|
||||
getAuth: async () => {
|
||||
if (!probableUser$.value) return { authToken: null }
|
||||
|
||||
await waitProbableLoginToConfirm()
|
||||
|
||||
return {
|
||||
authToken: getAuthIDToken(),
|
||||
}
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
subscriptionExchange({
|
||||
forwardSubscription: (operation) =>
|
||||
// @ts-expect-error: An issue with the Urql typing
|
||||
subscriptionClient.request(operation),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
export const client = ref(createHoppClient())
|
||||
|
||||
authIdToken$.subscribe(() => {
|
||||
client.value = createHoppClient()
|
||||
})
|
||||
|
||||
type MaybeRef<X> = X | Ref<X>
|
||||
|
||||
type UseQueryOptions<T = any, V = object> = {
|
||||
query: TypedDocumentNode<T, V>
|
||||
variables?: MaybeRef<V>
|
||||
|
||||
updateSubs?: MaybeRef<GraphQLRequest<any, object>[]>
|
||||
defer?: boolean
|
||||
pollDuration?: number | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper type for defining errors possible in a GQL operation
|
||||
*/
|
||||
export type GQLError<T extends string> =
|
||||
| {
|
||||
type: "network_error"
|
||||
error: Error
|
||||
}
|
||||
| {
|
||||
type: "gql_error"
|
||||
error: T
|
||||
}
|
||||
|
||||
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
_args: UseQueryOptions<DocType, DocVarType>
|
||||
) => {
|
||||
const stops: WatchStopHandle[] = []
|
||||
|
||||
const args = reactive(_args)
|
||||
|
||||
const loading: Ref<boolean> = ref(true)
|
||||
const isStale: Ref<boolean> = ref(true)
|
||||
const data: Ref<E.Either<GQLError<DocErrorType>, DocType>> = ref() as any
|
||||
|
||||
if (!args.updateSubs) set(args, "updateSubs", [])
|
||||
|
||||
const isPaused: Ref<boolean> = ref(args.defer ?? false)
|
||||
|
||||
const pollDuration: Ref<number | null> = ref(args.pollDuration ?? null)
|
||||
|
||||
const request: Ref<GraphQLRequest<DocType, DocVarType>> = ref(
|
||||
createRequest<DocType, DocVarType>(
|
||||
args.query,
|
||||
unref<DocVarType>(args.variables as any) as any
|
||||
)
|
||||
) as any
|
||||
|
||||
const source: Ref<Source<OperationResult> | undefined> = ref()
|
||||
|
||||
// Toggles between true and false to cause the polling operation to tick
|
||||
const pollerTick: Ref<boolean> = ref(true)
|
||||
|
||||
stops.push(
|
||||
watchEffect((onInvalidate) => {
|
||||
if (pollDuration.value !== null && !isPaused.value) {
|
||||
const handle = setInterval(() => {
|
||||
pollerTick.value = !pollerTick.value
|
||||
}, pollDuration.value)
|
||||
|
||||
onInvalidate(() => {
|
||||
clearInterval(handle)
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
stops.push(
|
||||
watchEffect(
|
||||
() => {
|
||||
const newRequest = createRequest<DocType, DocVarType>(
|
||||
args.query,
|
||||
unref<DocVarType>(args.variables as any) as any
|
||||
)
|
||||
|
||||
if (request.value.key !== newRequest.key) {
|
||||
request.value = newRequest
|
||||
}
|
||||
},
|
||||
{ flush: "pre" }
|
||||
)
|
||||
)
|
||||
|
||||
stops.push(
|
||||
watchEffect(
|
||||
() => {
|
||||
// Just listen to the polling ticks
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
pollerTick.value
|
||||
|
||||
source.value = !isPaused.value
|
||||
? client.value.executeQuery<DocType, DocVarType>(request.value, {
|
||||
requestPolicy: "cache-and-network",
|
||||
})
|
||||
: undefined
|
||||
},
|
||||
{ flush: "pre" }
|
||||
)
|
||||
)
|
||||
|
||||
watchSyncEffect((onInvalidate) => {
|
||||
if (source.value) {
|
||||
loading.value = true
|
||||
isStale.value = false
|
||||
|
||||
const invalidateStops = args.updateSubs!.map((sub) => {
|
||||
return wonkaPipe(
|
||||
client.value.executeSubscription(sub),
|
||||
onEnd(() => {
|
||||
if (source.value) execute()
|
||||
}),
|
||||
subscribe(() => {
|
||||
return execute()
|
||||
})
|
||||
).unsubscribe
|
||||
})
|
||||
|
||||
invalidateStops.push(
|
||||
wonkaPipe(
|
||||
source.value,
|
||||
onEnd(() => {
|
||||
loading.value = false
|
||||
isStale.value = false
|
||||
}),
|
||||
subscribe((res) => {
|
||||
if (res.operation.key === request.value.key) {
|
||||
data.value = pipe(
|
||||
// The target
|
||||
res.data as DocType | undefined,
|
||||
// Define what happens if data does not exist (it is an error)
|
||||
E.fromNullable(
|
||||
pipe(
|
||||
// Take the network error value
|
||||
res.error?.networkError,
|
||||
// If it null, set the left to the generic error name
|
||||
E.fromNullable(res.error?.message),
|
||||
E.match(
|
||||
// The left case (network error was null)
|
||||
(gqlErr) =>
|
||||
<GQLError<DocErrorType>>{
|
||||
type: "gql_error",
|
||||
error: parseGQLErrorString(
|
||||
gqlErr ?? ""
|
||||
) as DocErrorType,
|
||||
},
|
||||
// The right case (it was a GraphQL Error)
|
||||
(networkErr) =>
|
||||
<GQLError<DocErrorType>>{
|
||||
type: "network_error",
|
||||
error: networkErr,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
).unsubscribe
|
||||
)
|
||||
|
||||
onInvalidate(() => invalidateStops.forEach((unsub) => unsub()))
|
||||
}
|
||||
})
|
||||
|
||||
const execute = (updatedVars?: DocVarType) => {
|
||||
if (updatedVars) {
|
||||
if (isRef(args.variables)) {
|
||||
args.variables.value = updatedVars
|
||||
} else {
|
||||
set(args, "variables", updatedVars)
|
||||
}
|
||||
}
|
||||
|
||||
isPaused.value = false
|
||||
}
|
||||
|
||||
const response = reactive({
|
||||
loading,
|
||||
data,
|
||||
isStale,
|
||||
execute,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
const parseGQLErrorString = (s: string) =>
|
||||
s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s
|
||||
|
||||
export const runMutation = <
|
||||
DocType,
|
||||
DocVariables extends object | undefined,
|
||||
DocErrors extends string
|
||||
>(
|
||||
mutation: TypedDocumentNode<DocType, DocVariables>,
|
||||
variables?: DocVariables,
|
||||
additionalConfig?: Partial<OperationContext>
|
||||
): TE.TaskEither<GQLError<DocErrors>, DocType> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
client.value
|
||||
.mutation(mutation, variables, {
|
||||
requestPolicy: "cache-and-network",
|
||||
...additionalConfig,
|
||||
})
|
||||
.toPromise(),
|
||||
() => constVoid() as never // The mutation function can never fail, so this will never be called ;)
|
||||
),
|
||||
TE.chainEitherK((result) =>
|
||||
pipe(
|
||||
result.data,
|
||||
E.fromNullable(
|
||||
// Result is null
|
||||
pipe(
|
||||
result.error?.networkError,
|
||||
E.fromNullable(result.error?.message),
|
||||
E.match(
|
||||
// The left case (network error was null)
|
||||
(gqlErr) =>
|
||||
<GQLError<DocErrors>>{
|
||||
type: "gql_error",
|
||||
error: parseGQLErrorString(gqlErr ?? ""),
|
||||
},
|
||||
// The right case (it was a network error)
|
||||
(networkErr) =>
|
||||
<GQLError<DocErrors>>{
|
||||
type: "network_error",
|
||||
error: networkErr,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
export type UserQueryError = "user/not_found"
|
||||
|
||||
export type MyTeamsQueryError = "ea/not_invite_or_admin"
|
||||
@@ -1,8 +0,0 @@
|
||||
import { GraphCacheKeysConfig } from "../graphql"
|
||||
|
||||
export const keyDefs: GraphCacheKeysConfig = {
|
||||
User: (data) => data.uid!,
|
||||
TeamMember: (data) => data.membershipID!,
|
||||
Team: (data) => data.id!,
|
||||
TeamInvitation: (data) => data.id!,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { GraphCacheOptimisticUpdaters } from "../graphql"
|
||||
|
||||
export const optimisticDefs: GraphCacheOptimisticUpdaters = {
|
||||
deleteTeam: () => true,
|
||||
leaveTeam: () => true,
|
||||
renameTeam: ({ teamID, newName }) => ({
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
name: newName,
|
||||
}),
|
||||
removeTeamMember: () => true,
|
||||
revokeTeamInvitation: () => true,
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { GraphCacheResolvers } from "../graphql"
|
||||
|
||||
export const resolversDef: GraphCacheResolvers = {
|
||||
Query: {
|
||||
team: (_parent, { teamID }, _cache, _info) => ({
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
}),
|
||||
user: (_parent, { uid }, _cache, _info) => ({
|
||||
__typename: "User",
|
||||
uid,
|
||||
}),
|
||||
teamInvitation: (_parent, args, _cache, _info) => ({
|
||||
__typename: "TeamInvitation",
|
||||
id: args.inviteID,
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { GraphCacheUpdaters, MyTeamsDocument } from "../graphql"
|
||||
|
||||
export const updatesDef: GraphCacheUpdaters = {
|
||||
Subscription: {
|
||||
teamMemberAdded: (_r, { teamID }, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamMembers"
|
||||
)
|
||||
},
|
||||
teamMemberUpdated: (_r, { teamID }, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamMembers"
|
||||
)
|
||||
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"myRole"
|
||||
)
|
||||
},
|
||||
teamMemberRemoved: (_r, { teamID }, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamMembers"
|
||||
)
|
||||
},
|
||||
teamInvitationAdded: (_r, { teamID }, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamInvitations"
|
||||
)
|
||||
},
|
||||
teamInvitationRemoved: (_r, { teamID }, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamInvitations"
|
||||
)
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
deleteTeam: (_r, { teamID }, cache, _info) => {
|
||||
cache.updateQuery(
|
||||
{
|
||||
query: MyTeamsDocument,
|
||||
},
|
||||
(data) => {
|
||||
if (data) {
|
||||
data.myTeams = data.myTeams.filter((x) => x.id !== teamID)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
)
|
||||
|
||||
cache.invalidate({
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
})
|
||||
},
|
||||
leaveTeam: (_r, { teamID }, cache, _info) => {
|
||||
cache.updateQuery(
|
||||
{
|
||||
query: MyTeamsDocument,
|
||||
},
|
||||
(data) => {
|
||||
if (data) {
|
||||
data.myTeams = data.myTeams.filter((x) => x.id !== teamID)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
)
|
||||
|
||||
cache.invalidate({
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
})
|
||||
},
|
||||
createTeam: (result, _args, cache, _info) => {
|
||||
cache.updateQuery(
|
||||
{
|
||||
query: MyTeamsDocument,
|
||||
},
|
||||
(data) => {
|
||||
if (data) data.myTeams.push(result.createTeam as any)
|
||||
return data
|
||||
}
|
||||
)
|
||||
},
|
||||
removeTeamMember: (_result, { teamID, userUid }, cache) => {
|
||||
const newMembers = (
|
||||
(cache.resolve(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: teamID,
|
||||
},
|
||||
"teamMembers"
|
||||
) as string[]) ?? []
|
||||
)
|
||||
.map((x) => [x, cache.resolve(x, "user") as string])
|
||||
.map(([key, userKey]) => [key, cache.resolve(userKey, "uid") as string])
|
||||
.filter(([_key, uid]) => uid !== userUid)
|
||||
.map(([key]) => key)
|
||||
cache.link({ __typename: "Team", id: teamID }, "teamMembers", newMembers)
|
||||
},
|
||||
createTeamInvitation: (result, _args, cache, _info) => {
|
||||
cache.invalidate(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: result.createTeamInvitation.teamID!,
|
||||
},
|
||||
"teamInvitations"
|
||||
)
|
||||
},
|
||||
acceptTeamInvitation: (_result, _args, cache, _info) => {
|
||||
cache.invalidate({ __typename: "Query" }, "myTeams")
|
||||
},
|
||||
revokeTeamInvitation: (_result, args, cache, _info) => {
|
||||
const targetTeamID = cache.resolve(
|
||||
{
|
||||
__typename: "TeamInvitation",
|
||||
id: args.inviteID,
|
||||
},
|
||||
"teamID"
|
||||
)
|
||||
|
||||
if (typeof targetTeamID === "string") {
|
||||
const newInvites = (
|
||||
cache.resolve(
|
||||
{
|
||||
__typename: "Team",
|
||||
id: targetTeamID,
|
||||
},
|
||||
"teamInvitations"
|
||||
) as string[]
|
||||
).filter(
|
||||
(inviteKey) =>
|
||||
inviteKey !==
|
||||
cache.keyOfEntity({
|
||||
__typename: "TeamInvitation",
|
||||
id: args.inviteID,
|
||||
})
|
||||
)
|
||||
|
||||
cache.link(
|
||||
{ __typename: "Team", id: targetTeamID },
|
||||
"teamInvitations",
|
||||
newInvites
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
mutation AcceptTeamInvitation($inviteID: ID!) {
|
||||
acceptTeamInvitation(inviteID: $inviteID) {
|
||||
membershipID
|
||||
role
|
||||
user {
|
||||
uid
|
||||
displayName
|
||||
photoURL
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
mutation CreateTeam($name: String!) {
|
||||
createTeam(name: $name) {
|
||||
id
|
||||
name
|
||||
members {
|
||||
membershipID
|
||||
role
|
||||
user {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
photoURL
|
||||
}
|
||||
}
|
||||
myRole
|
||||
ownersCount
|
||||
editorsCount
|
||||
viewersCount
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mutation CreateTeamInvitation($inviteeEmail: String!, $inviteeRole: TeamMemberRole!, $teamID: ID!) {
|
||||
createTeamInvitation(inviteeRole: $inviteeRole, inviteeEmail: $inviteeEmail, teamID: $teamID) {
|
||||
id
|
||||
teamID
|
||||
creatorUid
|
||||
inviteeEmail
|
||||
inviteeRole
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mutation DeleteTeam($teamID: ID!) {
|
||||
deleteTeam(teamID: $teamID)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mutation LeaveTeam($teamID: ID!) {
|
||||
leaveTeam(teamID: $teamID)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) {
|
||||
removeTeamMember(userUid: $userUid, teamID: $teamID)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
mutation RenameTeam($newName: String!, $teamID: ID!) {
|
||||
renameTeam(newName: $newName, teamID: $teamID) {
|
||||
id
|
||||
name
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mutation RevokeTeamInvitation($inviteID: ID!) {
|
||||
revokeTeamInvitation(inviteID: $inviteID)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
mutation UpdateTeamMemberRole(
|
||||
$newRole: TeamMemberRole!,
|
||||
$userUid: ID!,
|
||||
$teamID: ID!
|
||||
) {
|
||||
updateTeamMemberRole(
|
||||
newRole: $newRole
|
||||
userUid: $userUid
|
||||
teamID: $teamID
|
||||
) {
|
||||
membershipID
|
||||
role
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
query GetInviteDetails($inviteID: ID!) {
|
||||
teamInvitation(inviteID: $inviteID) {
|
||||
id
|
||||
inviteeEmail
|
||||
inviteeRole
|
||||
team {
|
||||
id
|
||||
name
|
||||
}
|
||||
creator {
|
||||
uid
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
query GetTeam($teamID: ID!) {
|
||||
team(teamID: $teamID) {
|
||||
id
|
||||
name
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
query Me {
|
||||
me {
|
||||
uid
|
||||
displayName
|
||||
photoURL
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
query MyTeams {
|
||||
myTeams {
|
||||
id
|
||||
name
|
||||
myRole
|
||||
ownersCount
|
||||
teamMembers {
|
||||
membershipID
|
||||
user {
|
||||
photoURL
|
||||
displayName
|
||||
email
|
||||
uid
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
query GetPendingInvites($teamID: ID!) {
|
||||
team(teamID: $teamID) {
|
||||
id
|
||||
teamInvitations {
|
||||
inviteeRole
|
||||
inviteeEmail
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
subscription TeamInvitationAdded($teamID: ID!) {
|
||||
teamInvitationAdded(teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
subscription TeamInvitationRemoved($teamID: ID!) {
|
||||
teamInvitationRemoved(teamID: $teamID)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
subscription TeamMemberAdded($teamID: ID!) {
|
||||
teamMemberAdded(teamID: $teamID) {
|
||||
membershipID
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
subscription TeamMemberRemoved($teamID: ID!) {
|
||||
teamMemberRemoved(teamID: $teamID)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
subscription TeamMemberUpdated($teamID: ID!) {
|
||||
teamMemberUpdated(teamID: $teamID) {
|
||||
membershipID
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user