Compare commits

..

1 Commits

Author SHA1 Message Date
liyasthomas
da368a2d72 feat: show keys on press plugin 2021-09-24 19:42:10 +05:30
224 changed files with 5657 additions and 11912 deletions

View File

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

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

@@ -0,0 +1 @@
_

View File

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

View File

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

View File

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

View File

@@ -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/**"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,6 @@
0% {
@apply opacity-0;
}
100% {
@apply opacity-100;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,6 @@
focus-visible:bg-transparent focus-visible:border-dividerDark
"
:placeholder="$t('request.url')"
:disabled="connected"
@keyup.enter="onConnectClick"
/>
<ButtonPrimary

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -231,7 +231,7 @@ export default defineComponent({
&:hover,
&.active {
@apply bg-accentDark;
@apply bg-accent;
@apply text-accentContrast;
@apply cursor-pointer;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
export type UserQueryError = "user/not_found"
export type MyTeamsQueryError = "ea/not_invite_or_admin"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
mutation AcceptTeamInvitation($inviteID: ID!) {
acceptTeamInvitation(inviteID: $inviteID) {
membershipID
role
user {
uid
displayName
photoURL
email
}
}
}

View File

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

View File

@@ -1,9 +0,0 @@
mutation CreateTeamInvitation($inviteeEmail: String!, $inviteeRole: TeamMemberRole!, $teamID: ID!) {
createTeamInvitation(inviteeRole: $inviteeRole, inviteeEmail: $inviteeEmail, teamID: $teamID) {
id
teamID
creatorUid
inviteeEmail
inviteeRole
}
}

View File

@@ -1,3 +0,0 @@
mutation DeleteTeam($teamID: ID!) {
deleteTeam(teamID: $teamID)
}

View File

@@ -1,3 +0,0 @@
mutation LeaveTeam($teamID: ID!) {
leaveTeam(teamID: $teamID)
}

View File

@@ -1,3 +0,0 @@
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) {
removeTeamMember(userUid: $userUid, teamID: $teamID)
}

View File

@@ -1,13 +0,0 @@
mutation RenameTeam($newName: String!, $teamID: ID!) {
renameTeam(newName: $newName, teamID: $teamID) {
id
name
teamMembers {
membershipID
user {
uid
}
role
}
}
}

View File

@@ -1,3 +0,0 @@
mutation RevokeTeamInvitation($inviteID: ID!) {
revokeTeamInvitation(inviteID: $inviteID)
}

View File

@@ -1,14 +0,0 @@
mutation UpdateTeamMemberRole(
$newRole: TeamMemberRole!,
$userUid: ID!,
$teamID: ID!
) {
updateTeamMemberRole(
newRole: $newRole
userUid: $userUid
teamID: $teamID
) {
membershipID
role
}
}

View File

@@ -1,15 +0,0 @@
query GetInviteDetails($inviteID: ID!) {
teamInvitation(inviteID: $inviteID) {
id
inviteeEmail
inviteeRole
team {
id
name
}
creator {
uid
displayName
}
}
}

View File

@@ -1,14 +0,0 @@
query GetTeam($teamID: ID!) {
team(teamID: $teamID) {
id
name
teamMembers {
membershipID
user {
uid
email
}
role
}
}
}

View File

@@ -1,7 +0,0 @@
query Me {
me {
uid
displayName
photoURL
}
}

View File

@@ -1,18 +0,0 @@
query MyTeams {
myTeams {
id
name
myRole
ownersCount
teamMembers {
membershipID
user {
photoURL
displayName
email
uid
}
role
}
}
}

View File

@@ -1,10 +0,0 @@
query GetPendingInvites($teamID: ID!) {
team(teamID: $teamID) {
id
teamInvitations {
inviteeRole
inviteeEmail
id
}
}
}

View File

@@ -1,5 +0,0 @@
subscription TeamInvitationAdded($teamID: ID!) {
teamInvitationAdded(teamID: $teamID) {
id
}
}

View File

@@ -1,3 +0,0 @@
subscription TeamInvitationRemoved($teamID: ID!) {
teamInvitationRemoved(teamID: $teamID)
}

View File

@@ -1,5 +0,0 @@
subscription TeamMemberAdded($teamID: ID!) {
teamMemberAdded(teamID: $teamID) {
membershipID
}
}

View File

@@ -1,3 +0,0 @@
subscription TeamMemberRemoved($teamID: ID!) {
teamMemberRemoved(teamID: $teamID)
}

View File

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