Compare commits
2 Commits
2023.4.3
...
feat/env-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37153b6401 | ||
|
|
f3f9891017 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.3",
|
"version": "2023.4.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export class AuthService {
|
|||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
magicLink: `${url}/magic-link?token=${generatedTokens.token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const authCookieHandler = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!redirect) {
|
if (!redirect) {
|
||||||
return res.status(HttpStatus.OK).send();
|
res.status(HttpStatus.OK).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if redirectUrl is a whitelisted url
|
// check to see if redirectUrl is a whitelisted url
|
||||||
@@ -72,7 +72,7 @@ export const authCookieHandler = (
|
|||||||
// if it is not redirect by default to REDIRECT_URL
|
// if it is not redirect by default to REDIRECT_URL
|
||||||
redirectUrl = process.env.REDIRECT_URL;
|
redirectUrl = process.env.REDIRECT_URL;
|
||||||
|
|
||||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.3",
|
"version": "2023.4.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 595 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 331 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 352 KiB |
@@ -85,7 +85,6 @@ declare module '@vue/runtime-core' {
|
|||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
|
||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
@@ -131,6 +130,7 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
|
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
@@ -141,6 +141,7 @@ declare module '@vue/runtime-core' {
|
|||||||
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
||||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||||
|
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||||
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
||||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||||
@@ -164,7 +165,6 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
|
||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-if="currentUser.photoURL"
|
v-if="currentUser.photoURL"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
theme: 'tooltip',
|
theme: 'tooltip',
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-else
|
v-else
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="
|
:title="
|
||||||
|
|||||||
@@ -136,11 +136,11 @@ const requestName = ref(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [currentActiveTab.value, gqlRequestName.value],
|
() => [currentActiveTab.value.document.request.name, gqlRequestName.value],
|
||||||
() => {
|
() => {
|
||||||
if (props.mode === "rest") {
|
if (props.mode === "rest")
|
||||||
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
requestName.value = currentActiveTab.value.document.request.name
|
||||||
} else requestName.value = gqlRequestName.value
|
else requestName.value = gqlRequestName.value
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,78 +36,94 @@
|
|||||||
? IconCheck
|
? IconCheck
|
||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
|
class="my-2"
|
||||||
:active-info-icon="
|
:active-info-icon="
|
||||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||||
"
|
"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
setSelectedEnvironmentIndex({ type: 'NO_ENV_SELECTED' })
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div v-if="environmentType === 'my-environments'" class="flex flex-col">
|
<HoppSmartTabs
|
||||||
<hr v-if="myEnvironments.length > 0" />
|
v-model="selectedEnvTab"
|
||||||
<HoppSmartItem
|
styles="sticky overflow-x-auto flex-shrink-0 bg-primary z-10 top-0"
|
||||||
v-for="(gen, index) in myEnvironments"
|
render-inactive-tabs
|
||||||
:key="`gen-${index}`"
|
>
|
||||||
:label="gen.name"
|
<HoppSmartTab
|
||||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
:id="'my-environments'"
|
||||||
:active-info-icon="index === selectedEnv.index"
|
:label="`${t('environment.my_environments')}`"
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col">
|
|
||||||
<div
|
|
||||||
v-if="teamEnvLoading"
|
|
||||||
class="flex flex-col items-center justify-center p-4"
|
|
||||||
>
|
>
|
||||||
<HoppSmartSpinner class="my-4" />
|
<hr v-if="myEnvironments.length > 0" />
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
|
||||||
</div>
|
|
||||||
<hr v-if="teamEnvironmentList.length > 0" />
|
|
||||||
<div v-if="isTeamSelected" class="flex flex-col">
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-for="(gen, index) in teamEnvironmentList"
|
v-for="(gen, index) in myEnvironments"
|
||||||
:key="`gen-team-${index}`"
|
:key="`gen-${index}`"
|
||||||
:label="gen.environment.name"
|
:label="gen.name"
|
||||||
:info-icon="
|
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
||||||
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
:active-info-icon="index === selectedEnv.index"
|
||||||
"
|
|
||||||
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = {
|
setSelectedEnvironmentIndex({
|
||||||
type: 'TEAM_ENV',
|
type: 'MY_ENV',
|
||||||
teamEnvID: gen.id,
|
index,
|
||||||
teamID: gen.teamID,
|
})
|
||||||
environment: gen.environment,
|
|
||||||
}
|
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</HoppSmartTab>
|
||||||
<div
|
<HoppSmartTab
|
||||||
v-if="!teamEnvLoading && isAdapterError"
|
:id="'team-environments'"
|
||||||
class="flex flex-col items-center py-4"
|
:label="`${t('environment.team_environments')}`"
|
||||||
|
:disabled="
|
||||||
|
!isTeamSelected ||
|
||||||
|
teamEnvLoading ||
|
||||||
|
teamEnvironmentList.length === 0 ||
|
||||||
|
environmentType === 'my-environments'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
<div
|
||||||
{{ errorMessage }}
|
v-if="teamEnvLoading"
|
||||||
</div>
|
class="flex flex-col items-center justify-center p-4"
|
||||||
</div>
|
>
|
||||||
|
<HoppSmartSpinner class="my-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<hr v-if="teamEnvironmentList.length > 0" />
|
||||||
|
<div v-if="isTeamSelected" class="flex flex-col">
|
||||||
|
<HoppSmartItem
|
||||||
|
v-for="(gen, index) in teamEnvironmentList"
|
||||||
|
:key="`gen-team-${index}`"
|
||||||
|
:label="gen.environment.name"
|
||||||
|
:info-icon="
|
||||||
|
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
||||||
|
"
|
||||||
|
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
setSelectedEnvironmentIndex({
|
||||||
|
type: 'TEAM_ENV',
|
||||||
|
teamEnvID: gen.id,
|
||||||
|
teamID: gen.teamID,
|
||||||
|
environment: gen.environment,
|
||||||
|
})
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
@@ -134,13 +150,27 @@ const props = defineProps<{
|
|||||||
isTeamSelected: boolean
|
isTeamSelected: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const selectedEnvTab = ref<EnvironmentType>("my-environments")
|
||||||
|
|
||||||
const selectedEnvironmentIndex = useStream(
|
const selectedEnvironmentIndex = useStream(
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
{ type: "NO_ENV_SELECTED" },
|
{ type: "NO_ENV_SELECTED" },
|
||||||
setSelectedEnvironmentIndex
|
setSelectedEnvironmentIndex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.environmentType,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === "my-environments") {
|
||||||
|
selectedEnvTab.value = "my-environments"
|
||||||
|
} else {
|
||||||
|
selectedEnvTab.value = "team-environments"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const selectedEnv = computed(() => {
|
const selectedEnv = computed(() => {
|
||||||
|
console.log("selectedEnvironmentIndex", selectedEnvironmentIndex.value)
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
return {
|
return {
|
||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ watch(
|
|||||||
teamListFetched.value = true
|
teamListFetched.value = true
|
||||||
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
|
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
|
||||||
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||||
if (team) updateSelectedTeam(team)
|
if (team) {
|
||||||
|
updateSelectedTeam(team)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,7 @@ const switchToMyEnvironments = () => {
|
|||||||
adapter.changeTeamID(undefined)
|
adapter.changeTeamID(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
|
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||||
if (newSelectedTeam) {
|
if (newSelectedTeam) {
|
||||||
environmentType.value.selectedTeam = newSelectedTeam
|
environmentType.value.selectedTeam = newSelectedTeam
|
||||||
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
|
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
|
||||||
@@ -150,23 +152,30 @@ const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
|||||||
// Used to switch environment type and team when user switch workspace in the global workspace switcher
|
// Used to switch environment type and team when user switch workspace in the global workspace switcher
|
||||||
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team
|
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team
|
||||||
// If there is no teamID, switch to my environment
|
// If there is no teamID, switch to my environment
|
||||||
watch(workspace, (newWorkspace) => {
|
watch(
|
||||||
if (newWorkspace.type === "personal") {
|
() => workspace.value.teamID,
|
||||||
switchToMyEnvironments()
|
(teamID) => {
|
||||||
setSelectedEnvironmentIndex({
|
if (!teamID) {
|
||||||
type: "NO_ENV_SELECTED",
|
switchToMyEnvironments()
|
||||||
})
|
|
||||||
} else if (newWorkspace.type === "team") {
|
|
||||||
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
|
|
||||||
updateSelectedTeam(team)
|
|
||||||
|
|
||||||
if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
|
// If the user selected a team environment, and then switch to personal workspace,
|
||||||
setSelectedEnvironmentIndex({
|
// we need to reset the selected environment
|
||||||
type: "NO_ENV_SELECTED",
|
if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||||
})
|
setSelectedEnvironmentIndex({
|
||||||
|
type: "NO_ENV_SELECTED",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (teamID) {
|
||||||
|
const team = myTeams.value?.find((t) => t.id === teamID)
|
||||||
|
if (team) {
|
||||||
|
updateSelectedTeam(team)
|
||||||
|
setSelectedEnvironmentIndex({
|
||||||
|
type: "NO_ENV_SELECTED",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentUser.value,
|
() => currentUser.value,
|
||||||
|
|||||||
@@ -229,10 +229,11 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useStreamSubscriber } from "@composables/stream"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
|
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||||
import { computed, onBeforeUnmount, ref } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||||
@@ -310,6 +311,39 @@ const clearAll = ref<any | null>(null)
|
|||||||
const copyRequestAction = ref<any | null>(null)
|
const copyRequestAction = ref<any | null>(null)
|
||||||
const saveRequestAction = ref<any | null>(null)
|
const saveRequestAction = ref<any | null>(null)
|
||||||
|
|
||||||
|
// Update Nuxt Loading bar
|
||||||
|
watch(loading, () => {
|
||||||
|
if (loading.value) {
|
||||||
|
startPageProgress()
|
||||||
|
} else {
|
||||||
|
completePageProgress()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: make this oAuthURL() work
|
||||||
|
|
||||||
|
// function oAuthURL() {
|
||||||
|
// const auth = useReadonlyStream(props.request.auth$, {
|
||||||
|
// authType: "none",
|
||||||
|
// authActive: true,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||||
|
|
||||||
|
// onBeforeMount(async () => {
|
||||||
|
// try {
|
||||||
|
// const tokenInfo = await oauthRedirect()
|
||||||
|
// if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||||
|
// if (typeof tokenInfo === "object") {
|
||||||
|
// oauth2Token.value = tokenInfo.access_token
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // eslint-disable-next-line no-empty
|
||||||
|
// } catch (_) {}
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
toast.error(`${t("empty.endpoint")}`)
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
@@ -540,10 +574,6 @@ const saveRequest = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (loading.value) cancelRequest()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineActionHandler("request.send-cancel", () => {
|
defineActionHandler("request.send-cancel", () => {
|
||||||
if (!loading.value) newSendRequest()
|
if (!loading.value) newSendRequest()
|
||||||
else cancelRequest()
|
else cancelRequest()
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
|
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
@@ -33,4 +34,9 @@ const hasResponse = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const loading = computed(() => tab.value.response?.type === "loading")
|
const loading = computed(() => tab.value.response?.type === "loading")
|
||||||
|
|
||||||
|
watch(loading, (isLoading) => {
|
||||||
|
if (isLoading) startPageProgress()
|
||||||
|
else completePageProgress()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
tabindex="0"
|
||||||
|
class="relative flex items-center justify-center overflow-visible cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-primaryDark"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="url"
|
||||||
|
class="absolute object-cover object-center transition bg-primaryDark"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
:src="url"
|
||||||
|
:alt="alt"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="absolute flex items-center justify-center object-cover object-center transition bg-primaryDark text-accentContrast"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
:style="`background-color: ${initial ? toHex(initial) : '#480000'}`"
|
||||||
|
>
|
||||||
|
<template v-if="initial && initial.charAt(0).toUpperCase()">
|
||||||
|
{{ initial.charAt(0).toUpperCase() }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<icon-lucide-user v-else></icon-lucide-user>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="indicator"
|
||||||
|
class="border-primary border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"
|
||||||
|
:class="[`rounded-${rounded}`, indicatorStyles]"
|
||||||
|
></span>
|
||||||
|
<!-- w-5 h-5 rounded-lg -->
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from "vue"
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: "Profile picture",
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
indicatorStyles: {
|
||||||
|
type: String,
|
||||||
|
default: "bg-green-500",
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: String,
|
||||||
|
default: "full",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "5",
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
type: String as PropType<string | undefined | null>,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toHex(initial: string) {
|
||||||
|
let hash = 0
|
||||||
|
if (initial.length === 0) return hash
|
||||||
|
for (let i = 0; i < initial.length; i++) {
|
||||||
|
hash = initial.charCodeAt(i) + ((hash << 5) - hash)
|
||||||
|
hash = hash & hash
|
||||||
|
}
|
||||||
|
let color = "#"
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 255
|
||||||
|
color += `00${value.toString(16)}`.slice(-2)
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -34,7 +34,6 @@ import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
|||||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -220,7 +219,6 @@ onMounted(() => {
|
|||||||
if (editor.value) {
|
if (editor.value) {
|
||||||
if (!view.value) initView(editor.value)
|
if (!view.value) initView(editor.value)
|
||||||
if (props.selectTextOnMount) triggerTextSelection()
|
if (props.selectTextOnMount) triggerTextSelection()
|
||||||
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:key="`member-${index}`"
|
:key="`member-${index}`"
|
||||||
class="inline-flex"
|
class="inline-flex"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-if="member.user.photoURL"
|
v-if="member.user.photoURL"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:url="member.user.photoURL"
|
:url="member.user.photoURL"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
class="ring-primary ring-2"
|
class="ring-primary ring-2"
|
||||||
@click="handleClick()"
|
@click="handleClick()"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-else
|
v-else
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="getUserName(member)"
|
:title="getUserName(member)"
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
baseHighlightStyle,
|
baseHighlightStyle,
|
||||||
} from "@helpers/editor/themes/baseTheme"
|
} from "@helpers/editor/themes/baseTheme"
|
||||||
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
|
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
|
||||||
import { platform } from "~/platform"
|
|
||||||
// TODO: Migrate from legacy mode
|
// TODO: Migrate from legacy mode
|
||||||
|
|
||||||
type ExtendedEditorConfig = {
|
type ExtendedEditorConfig = {
|
||||||
@@ -268,7 +267,6 @@ export function useCodemirror(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (el.value) {
|
if (el.value) {
|
||||||
if (!view.value) initView(el.value)
|
if (!view.value) initView(el.value)
|
||||||
platform.ui?.onCodemirrorInstanceMount?.(el.value)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,7 @@
|
|||||||
>
|
>
|
||||||
<Pane class="flex flex-1 !overflow-auto">
|
<Pane class="flex flex-1 !overflow-auto">
|
||||||
<main class="flex flex-1 w-full" role="main">
|
<main class="flex flex-1 w-full" role="main">
|
||||||
<RouterView
|
<RouterView v-slot="{ Component }" class="flex flex-1">
|
||||||
v-slot="{ Component }"
|
|
||||||
class="flex flex-1 min-w-0"
|
|
||||||
>
|
|
||||||
<Transition name="fade" mode="out-in" appear>
|
<Transition name="fade" mode="out-in" appear>
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -37,27 +37,13 @@ type EnvironmentStore = typeof defaultEnvironmentsState
|
|||||||
|
|
||||||
const dispatchers = defineDispatchers({
|
const dispatchers = defineDispatchers({
|
||||||
setSelectedEnvironmentIndex(
|
setSelectedEnvironmentIndex(
|
||||||
store: EnvironmentStore,
|
_: EnvironmentStore,
|
||||||
{
|
{
|
||||||
selectedEnvironmentIndex,
|
selectedEnvironmentIndex,
|
||||||
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
|
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
|
||||||
) {
|
) {
|
||||||
if (selectedEnvironmentIndex.type === "MY_ENV") {
|
return {
|
||||||
if (store.environments[selectedEnvironmentIndex.index]) {
|
selectedEnvironmentIndex,
|
||||||
return {
|
|
||||||
selectedEnvironmentIndex,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
selectedEnvironmentIndex: {
|
|
||||||
type: "NO_ENV_SELECTED",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
selectedEnvironmentIndex,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
appendEnvironments(
|
appendEnvironments(
|
||||||
@@ -339,22 +325,21 @@ export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe(
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
export const currentEnvironment$: Observable<Environment | undefined> =
|
export const currentEnvironment$ = environmentsStore.subject$.pipe(
|
||||||
environmentsStore.subject$.pipe(
|
map(({ environments, selectedEnvironmentIndex }) => {
|
||||||
map(({ environments, selectedEnvironmentIndex }) => {
|
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
const env: Environment = {
|
||||||
const env: Environment = {
|
name: "No environment",
|
||||||
name: "No environment",
|
variables: [],
|
||||||
variables: [],
|
|
||||||
}
|
|
||||||
return env
|
|
||||||
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
|
||||||
return environments[selectedEnvironmentIndex.index]
|
|
||||||
} else {
|
|
||||||
return selectedEnvironmentIndex.environment
|
|
||||||
}
|
}
|
||||||
})
|
return env
|
||||||
)
|
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||||
|
return environments[selectedEnvironmentIndex.index]
|
||||||
|
} else {
|
||||||
|
return selectedEnvironmentIndex.environment
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export type AggregateEnvironment = {
|
export type AggregateEnvironment = {
|
||||||
key: string
|
key: string
|
||||||
@@ -373,7 +358,7 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
|||||||
map(([selectedEnv, globalVars]) => {
|
map(([selectedEnv, globalVars]) => {
|
||||||
const results: AggregateEnvironment[] = []
|
const results: AggregateEnvironment[] = []
|
||||||
|
|
||||||
selectedEnv?.variables.forEach(({ key, value }) =>
|
selectedEnv.variables.forEach(({ key, value }) =>
|
||||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
results.push({ key, value, sourceEnv: selectedEnv.name })
|
||||||
)
|
)
|
||||||
globalVars.forEach(({ key, value }) =>
|
globalVars.forEach(({ key, value }) =>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
></div>
|
></div>
|
||||||
<div class="flex flex-col justify-between px-4 space-y-8 md:flex-row">
|
<div class="flex flex-col justify-between px-4 space-y-8 md:flex-row">
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-if="currentUser.photoURL"
|
v-if="currentUser.photoURL"
|
||||||
:url="currentUser.photoURL"
|
:url="currentUser.photoURL"
|
||||||
:alt="
|
:alt="
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
size="16"
|
size="16"
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-else
|
v-else
|
||||||
:initial="currentUser.displayName || currentUser.email"
|
:initial="currentUser.displayName || currentUser.email"
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ export type UIPlatformDef = {
|
|||||||
paddingTop?: Ref<string>
|
paddingTop?: Ref<string>
|
||||||
paddingLeft?: Ref<string>
|
paddingLeft?: Ref<string>
|
||||||
}
|
}
|
||||||
onCodemirrorInstanceMount?: (element: HTMLElement) => void
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/selfhost-web",
|
"name": "@hoppscotch/selfhost-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.3",
|
"version": "2023.4.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-sh-admin",
|
"name": "hoppscotch-sh-admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.3",
|
"version": "2023.4.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
arrow
|
arrow
|
||||||
:on-shown="() => tippyActions!.focus()"
|
:on-shown="() => tippyActions!.focus()"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-if="currentUser.photoURL"
|
v-if="currentUser.photoURL"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
theme: 'tooltip',
|
theme: 'tooltip',
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
:alt="currentUser.displayName ?? 'No Name'"
|
:alt="currentUser.displayName ?? 'No Name'"
|
||||||
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPicture
|
<ProfilePicture
|
||||||
v-else
|
v-else
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||||
|
|||||||
@@ -33,40 +33,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
withDefaults(
|
import { defineComponent, PropType } from 'vue';
|
||||||
defineProps<{
|
|
||||||
url: string
|
|
||||||
alt: string
|
|
||||||
indicator: boolean
|
|
||||||
indicatorStyles: string
|
|
||||||
rounded: string
|
|
||||||
size: string
|
|
||||||
initial: string | undefined | null
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
url: "",
|
|
||||||
alt: "Profile picture",
|
|
||||||
indicator: false,
|
|
||||||
indicatorStyles: "bg-green-500",
|
|
||||||
rounded: "full",
|
|
||||||
size: "5",
|
|
||||||
initial: "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const toHex = (initial: string) => {
|
export default defineComponent({
|
||||||
let hash = 0
|
props: {
|
||||||
if (initial.length === 0) return hash
|
url: {
|
||||||
for (let i = 0; i < initial.length; i++) {
|
type: String,
|
||||||
hash = initial.charCodeAt(i) + ((hash << 5) - hash)
|
default: '',
|
||||||
hash = hash & hash
|
},
|
||||||
}
|
alt: {
|
||||||
let color = "#"
|
type: String,
|
||||||
for (let i = 0; i < 3; i++) {
|
default: 'Profile picture',
|
||||||
const value = (hash >> (i * 8)) & 255
|
},
|
||||||
color += `00${value.toString(16)}`.slice(-2)
|
indicator: {
|
||||||
}
|
type: Boolean,
|
||||||
return color
|
default: false,
|
||||||
}
|
},
|
||||||
|
indicatorStyles: {
|
||||||
|
type: String,
|
||||||
|
default: 'bg-green-500',
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: String,
|
||||||
|
default: 'full',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '5',
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
type: String as PropType<string | undefined | null>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toHex(initial: string) {
|
||||||
|
let hash = 0;
|
||||||
|
if (initial.length === 0) return hash;
|
||||||
|
for (let i = 0; i < initial.length; i++) {
|
||||||
|
hash = initial.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 255;
|
||||||
|
color += `00${value.toString(16)}`.slice(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -6,7 +6,7 @@ const isAdmin = () => {
|
|||||||
return user ? user.isAdmin : false;
|
return user ? user.isAdmin : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GUEST_ROUTES = ['index', 'enter'];
|
const GUEST_ROUTES = ['index', 'magic-link'];
|
||||||
|
|
||||||
const isGuestRoute = (to: unknown) => GUEST_ROUTES.includes(to as string);
|
const isGuestRoute = (to: unknown) => GUEST_ROUTES.includes(to as string);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||||
<div
|
<div
|
||||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight group-tabs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
||||||
ref="scrollContainer"
|
ref="scrollContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between divide-x divide-dividerLight"
|
class="flex justify-between divide-x divide-divider"
|
||||||
@wheel.prevent="scroll"
|
@wheel.prevent="scroll"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
<template #item="{ element: [tabID, tabMeta] }">
|
<template #item="{ element: [tabID, tabMeta] }">
|
||||||
<button
|
<button
|
||||||
:key="`removable-tab-${tabID}`"
|
:key="`removable-tab-${tabID}`"
|
||||||
:id="`removable-tab-${tabID}`"
|
class="tab group px-2"
|
||||||
class="px-2 tab group"
|
|
||||||
:class="[{ active: modelValue === tabID }]"
|
:class="[{ active: modelValue === tabID }]"
|
||||||
:aria-label="tabMeta.label || ''"
|
:aria-label="tabMeta.label || ''"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -40,14 +39,14 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!tabMeta.tabhead"
|
v-if="!tabMeta.tabhead"
|
||||||
class="w-full px-2 text-left truncate"
|
class="truncate w-full text-left px-2"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ tabMeta.label }}
|
{{ tabMeta.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="w-full text-left truncate">
|
<div v-else class="truncate w-full text-left">
|
||||||
<component :is="tabMeta.tabhead" />
|
<component :is="tabMeta.tabhead" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
},
|
},
|
||||||
'close',
|
'close',
|
||||||
]"
|
]"
|
||||||
class="rounded !p-0.25"
|
class="!p-0.25 rounded"
|
||||||
@click.stop="emit('removeTab', tabID)"
|
@click.stop="emit('removeTab', tabID)"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -81,33 +80,33 @@
|
|||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-14"
|
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-8"
|
||||||
>
|
>
|
||||||
<span
|
<slot name="actions">
|
||||||
v-if="canAddNewTab"
|
<span
|
||||||
class="flex items-center justify-center h-full px-3 bg-primaryLight z-8"
|
v-if="canAddNewTab"
|
||||||
>
|
class="flex items-center justify-center px-3 bg-primaryLight z-8 h-full"
|
||||||
<HoppButtonSecondary
|
>
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
<HoppButtonSecondary
|
||||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:icon="IconPlus"
|
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||||
class="rounded create-new-tab !text-secondaryDark !p-1"
|
:icon="IconPlus"
|
||||||
filled
|
class="rounded !text-secondaryDark !p-1"
|
||||||
@click="addTab"
|
filled
|
||||||
/>
|
@click="addTab"
|
||||||
</span>
|
/>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot name="actions" />
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
:max="MAX_SCROLL_VALUE"
|
:max="MAX_SCROLL_VALUE"
|
||||||
v-model="thumbPosition"
|
v-model="thumbPosition"
|
||||||
class="absolute bottom-0 left-0 hidden slider"
|
class="slider absolute bottom-0 hidden left-0"
|
||||||
:class="{
|
:class="{
|
||||||
'!block': scrollThumb.show,
|
'!block': scrollThumb.show,
|
||||||
}"
|
}"
|
||||||
@@ -132,15 +131,7 @@ import { pipe } from "fp-ts/function"
|
|||||||
import { not } from "fp-ts/Predicate"
|
import { not } from "fp-ts/Predicate"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import {
|
import { ref, ComputedRef, computed, provide, inject, watch } from "vue"
|
||||||
ref,
|
|
||||||
ComputedRef,
|
|
||||||
computed,
|
|
||||||
provide,
|
|
||||||
inject,
|
|
||||||
watch,
|
|
||||||
nextTick,
|
|
||||||
} from "vue"
|
|
||||||
import { useElementSize } from "@vueuse/core"
|
import { useElementSize } from "@vueuse/core"
|
||||||
import type { Slot } from "vue"
|
import type { Slot } from "vue"
|
||||||
import draggable from "vuedraggable-es"
|
import draggable from "vuedraggable-es"
|
||||||
@@ -195,10 +186,9 @@ const throwError = (message: string): never => {
|
|||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAB_WIDTH = 184
|
|
||||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||||
const tabStyles = computed(() => ({
|
const tabStyles = computed(() => ({
|
||||||
maxWidth: `${tabEntries.value.length * TAB_WIDTH}px`,
|
maxWidth: `${tabEntries.value.length * 184}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minWidth: "0px",
|
minWidth: "0px",
|
||||||
// transition: "max-width 0.2s",
|
// transition: "max-width 0.2s",
|
||||||
@@ -302,49 +292,6 @@ watch(thumbPosition, (newVal) => {
|
|||||||
const maxScroll = scrollWidth - clientWidth
|
const maxScroll = scrollWidth - clientWidth
|
||||||
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
|
||||||
* Watch TabID changes
|
|
||||||
* and scroll to the tab if it's not visible
|
|
||||||
*/
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(tabID) => {
|
|
||||||
nextTick(() => {
|
|
||||||
const element = document.getElementById(`removable-tab-${tabID}`)
|
|
||||||
|
|
||||||
const changeThumbPosition: IntersectionObserverCallback = (
|
|
||||||
entries,
|
|
||||||
observer
|
|
||||||
) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.target === element && entry.intersectionRatio >= 1.0) {
|
|
||||||
// Element is visible now. Stop listening for intersection changes
|
|
||||||
observer.disconnect()
|
|
||||||
|
|
||||||
// We still need setTimeout here because the element might not be fully in position yet
|
|
||||||
setTimeout(() => {
|
|
||||||
const { scrollWidth, clientWidth, scrollLeft } =
|
|
||||||
scrollContainer.value!
|
|
||||||
const maxScroll = scrollWidth - clientWidth
|
|
||||||
thumbPosition.value = (scrollLeft / maxScroll) * MAX_SCROLL_VALUE
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let observer = new IntersectionObserver(changeThumbPosition, {
|
|
||||||
root: null,
|
|
||||||
rootMargin: "0px",
|
|
||||||
threshold: 1.0,
|
|
||||||
})
|
|
||||||
observer.observe(element!)
|
|
||||||
|
|
||||||
element?.scrollIntoView({ behavior: "smooth", inline: "center" })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -389,13 +336,6 @@ watch(
|
|||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@apply bg-primary;
|
@apply bg-primary;
|
||||||
@apply before: bg-accent;
|
@apply before: bg-accent;
|
||||||
@apply after: absolute;
|
|
||||||
@apply after: inset-x-0;
|
|
||||||
@apply after: bottom-0;
|
|
||||||
@apply after: bg-primary;
|
|
||||||
@apply after: z-12;
|
|
||||||
@apply after: h-0.25;
|
|
||||||
@apply after: content-DEFAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
@@ -408,16 +348,6 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-new-tab {
|
|
||||||
@apply after: absolute;
|
|
||||||
@apply after: inset-x-0;
|
|
||||||
@apply after: bottom-0;
|
|
||||||
@apply after: bg-dividerLight;
|
|
||||||
@apply after: z-14;
|
|
||||||
@apply after: h-0.25;
|
|
||||||
@apply after: content-DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
$slider-height: 4px;
|
$slider-height: 4px;
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -18,4 +18,3 @@ export { default as HoppSmartTabs } from "./Tabs.vue"
|
|||||||
export { default as HoppSmartToggle } from "./Toggle.vue"
|
export { default as HoppSmartToggle } from "./Toggle.vue"
|
||||||
export { default as HoppSmartWindow } from "./Window.vue"
|
export { default as HoppSmartWindow } from "./Window.vue"
|
||||||
export { default as HoppSmartWindows } from "./Windows.vue"
|
export { default as HoppSmartWindows } from "./Windows.vue"
|
||||||
export { default as HoppSmartPicture } from "./Picture.vue"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user