Compare commits

...

10 Commits

Author SHA1 Message Date
Nivedin
44ef51644e chore: fix type issues 2023-05-11 00:33:59 +05:30
Nivedin
252fe9e5d6 fix: reset env when workspace change 2023-05-11 00:33:59 +05:30
Nivedin
a52ef2de9a refactor: move env selector to a component 2023-05-11 00:33:59 +05:30
Liyas Thomas
f04149d971 docs: updated screenshots (#3046) 2023-05-11 00:33:59 +05:30
Anwarul Islam
ed9f412c5c fix: tab system breaks when a new tab is created while waiting for response in another tab (#3031) 2023-05-10 19:16:28 +05:30
Akash K
8765c1a8ac fix: invalid environment index can break the app (#3041) 2023-05-10 19:14:16 +05:30
Akash K
b2693d6ba2 chore: add onCodemirrorInstanceMount hook to platform (#3043) 2023-05-10 18:59:57 +05:30
Anwarul Islam
d9ed10bcca feat: scroll to show the new active tab header (#3013)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-09 15:58:44 +05:30
Mir Arif Hasan
87685b8cd9 fix: magic link URL (#3028) 2023-05-09 15:55:38 +05:30
Mir Arif Hasan
00fcc78f85 fix: returning response from authCookieHandler (#3025) 2023-05-09 15:55:01 +05:30
16 changed files with 333 additions and 273 deletions

View File

@@ -232,7 +232,7 @@ export class AuthService {
template: 'code-your-own',
variables: {
inviteeEmail: email,
magicLink: `${url}/magic-link?token=${generatedTokens.token}`,
magicLink: `${url}/enter?token=${generatedTokens.token}`,
},
});

View File

@@ -63,7 +63,7 @@ export const authCookieHandler = (
});
if (!redirect) {
res.status(HttpStatus.OK).send();
return res.status(HttpStatus.OK).send();
}
// 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
redirectUrl = process.env.REDIRECT_URL;
res.status(HttpStatus.OK).redirect(redirectUrl);
return res.status(HttpStatus.OK).redirect(redirectUrl);
};
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 KiB

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View File

@@ -57,6 +57,7 @@ declare module '@vue/runtime-core' {
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']

View File

@@ -136,11 +136,11 @@ const requestName = ref(
)
watch(
() => [currentActiveTab.value.document.request.name, gqlRequestName.value],
() => [currentActiveTab.value, gqlRequestName.value],
() => {
if (props.mode === "rest")
requestName.value = currentActiveTab.value.document.request.name
else requestName.value = gqlRequestName.value
if (props.mode === "rest") {
requestName.value = currentActiveTab.value?.document.request.name ?? ""
} else requestName.value = gqlRequestName.value
}
)

View File

@@ -0,0 +1,174 @@
<template>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent border-b border-dividerLight select-wrapper"
>
<HoppButtonSecondary
v-if="selectedEnv.type !== 'NO_ENV_SELECTED'"
:label="selectedEnv.name"
class="flex-1 !justify-start pr-8 rounded-none"
/>
<HoppButtonSecondary
v-else
:label="`${t('environment.select')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
? IconCheck
: undefined
"
class="my-2"
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
setSelectedEnvironmentIndex({ type: 'NO_ENV_SELECTED' })
hide()
}
"
/>
<div v-if="environmentType === 'my-environments'" class="flex flex-col">
<hr v-if="myEnvironments.length > 0" />
<HoppSmartItem
v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
@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" />
<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="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
</div>
<div
v-if="!teamEnvLoading && isAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ errorMessage }}
</div>
</div>
</div>
</template>
</tippy>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue"
import IconCheck from "~icons/lucide/check"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useStream } from "~/composables/stream"
import {
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
const t = useI18n()
type EnvironmentType = "my-environments" | "team-environments"
const props = defineProps<{
environmentType: EnvironmentType
myEnvironments: Environment[]
teamEnvironmentList: TeamEnvironment[]
teamEnvLoading: boolean
isAdapterError: boolean
errorMessage: GQLError<string>
isTeamSelected: boolean
}>()
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: props.myEnvironments[selectedEnvironmentIndex.value.index].name,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = props.teamEnvironmentList.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
})
// Template refs
const tippyActions = ref<TippyComponent | null>(null)
</script>

View File

@@ -4,153 +4,15 @@
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
>
<WorkspaceCurrent :section="t('tab.environments')" />
<tippy
v-if="environmentType.type === 'my-environments'"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent border-b border-dividerLight select-wrapper"
>
<HoppButtonSecondary
v-if="
selectedEnv.type === 'MY_ENV' && selectedEnv.index !== undefined
"
:label="myEnvironments[selectedEnv.index].name"
class="flex-1 !justify-start pr-8 rounded-none"
/>
<HoppButtonSecondary
v-else
:label="`${t('environment.select')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type !== 'MY_ENV'
? IconCheck
: undefined
"
:active-info-icon="selectedEnvironmentIndex.type !== 'MY_ENV'"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
"
/>
<hr v-if="myEnvironments.length > 0" />
<HoppSmartItem
v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
@click="
() => {
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
hide()
}
"
/>
</div>
</template>
</tippy>
<tippy v-else interactive trigger="click" theme="popover">
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent border-b border-dividerLight select-wrapper"
>
<HoppButtonSecondary
v-if="selectedEnv.name"
:label="selectedEnv.name"
class="flex-1 !justify-start pr-8 rounded-none"
/>
<HoppButtonSecondary
v-else
:label="`${t('environment.select')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
role="menu"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type !== 'TEAM_ENV'
? IconCheck
: undefined
"
:active-info-icon="selectedEnvironmentIndex.type !== 'TEAM_ENV'"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
"
/>
<div
v-if="loading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<hr v-if="teamEnvironmentList.length > 0" />
<div
v-if="environmentType.selectedTeam !== undefined"
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="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
</div>
<div
v-if="!loading && adapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(adapterError) }}
</div>
</div>
</template>
</tippy>
<EnvironmentsSelector
:environment-type="environmentType.type"
:my-environments="myEnvironments"
:team-env-loading="loading"
:team-environment-list="teamEnvironmentList"
:is-adapter-error="adapterError !== null"
:error-message="adapterError ? getErrorMessage(adapterError) : ''"
:is-team-selected="environmentType.selectedTeam !== undefined"
/>
<EnvironmentsMyEnvironment
environment-index="Global"
:environment="globalEnvironment"
@@ -191,8 +53,6 @@ import {
} from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import IconCheck from "~icons/lucide/check"
import { TippyComponent } from "vue-tippy"
import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -291,14 +151,20 @@ const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// 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
watch(
() => workspace.value.teamID,
() => workspace.value.type === "team" && workspace.value.teamID,
(teamID) => {
if (!teamID) {
switchToMyEnvironments()
} else if (teamID) {
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else {
const team = myTeams.value?.find((t) => t.id === teamID)
if (team) {
updateSelectedTeam(team)
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
}
}
}
@@ -388,33 +254,6 @@ watch(
{ deep: true }
)
const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
})
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
@@ -427,7 +266,4 @@ const getErrorMessage = (err: GQLError<string>) => {
}
}
}
// Template refs
const tippyActions = ref<TippyComponent | null>(null)
</script>

View File

@@ -229,11 +229,10 @@ import { useI18n } from "@composables/i18n"
import { useSetting } from "@composables/settings"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
import { refAutoReset, useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either"
import { isLeft, isRight } from "fp-ts/lib/Either"
import { computed, ref, watch } from "vue"
import { computed, onBeforeUnmount, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
@@ -311,39 +310,6 @@ const clearAll = ref<any | null>(null)
const copyRequestAction = 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 () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
@@ -574,6 +540,10 @@ const saveRequest = () => {
}
}
onBeforeUnmount(() => {
if (loading.value) cancelRequest()
})
defineActionHandler("request.send-cancel", () => {
if (!loading.value) newSendRequest()
else cancelRequest()

View File

@@ -10,8 +10,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import { computed, ref } from "vue"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { useVModel } from "@vueuse/core"
@@ -34,9 +33,4 @@ const hasResponse = computed(
)
const loading = computed(() => tab.value.response?.type === "loading")
watch(loading, (isLoading) => {
if (isLoading) startPageProgress()
else completePageProgress()
})
</script>

View File

@@ -34,6 +34,7 @@ import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
const props = withDefaults(
defineProps<{
@@ -219,6 +220,7 @@ onMounted(() => {
if (editor.value) {
if (!view.value) initView(editor.value)
if (props.selectTextOnMount) triggerTextSelection()
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
}
})

View File

@@ -38,6 +38,7 @@ import {
baseHighlightStyle,
} from "@helpers/editor/themes/baseTheme"
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
import { platform } from "~/platform"
// TODO: Migrate from legacy mode
type ExtendedEditorConfig = {
@@ -267,6 +268,7 @@ export function useCodemirror(
onMounted(() => {
if (el.value) {
if (!view.value) initView(el.value)
platform.ui?.onCodemirrorInstanceMount?.(el.value)
}
})

View File

@@ -37,13 +37,22 @@ type EnvironmentStore = typeof defaultEnvironmentsState
const dispatchers = defineDispatchers({
setSelectedEnvironmentIndex(
_: EnvironmentStore,
store: EnvironmentStore,
{
selectedEnvironmentIndex,
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
) {
return {
selectedEnvironmentIndex,
if (
selectedEnvironmentIndex.type === "MY_ENV" &&
!!store.environments[selectedEnvironmentIndex.index]
) {
return {
selectedEnvironmentIndex,
}
} else {
return {
type: "NO_ENV_SELECTED",
}
}
},
appendEnvironments(
@@ -325,21 +334,22 @@ export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe(
distinctUntilChanged()
)
export const currentEnvironment$ = environmentsStore.subject$.pipe(
map(({ environments, selectedEnvironmentIndex }) => {
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
const env: Environment = {
name: "No environment",
variables: [],
export const currentEnvironment$: Observable<Environment | undefined> =
environmentsStore.subject$.pipe(
map(({ environments, selectedEnvironmentIndex }) => {
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
const env: Environment = {
name: "No environment",
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 = {
key: string
@@ -358,7 +368,7 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
map(([selectedEnv, globalVars]) => {
const results: AggregateEnvironment[] = []
selectedEnv.variables.forEach(({ key, value }) =>
selectedEnv?.variables.forEach(({ key, value }) =>
results.push({ key, value, sourceEnv: selectedEnv.name })
)
globalVars.forEach(({ key, value }) =>

View File

@@ -5,4 +5,5 @@ export type UIPlatformDef = {
paddingTop?: Ref<string>
paddingLeft?: Ref<string>
}
onCodemirrorInstanceMount?: (element: HTMLElement) => void
}

View File

@@ -1,14 +1,14 @@
<template>
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
<div
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight group-tabs"
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
>
<div
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
ref="scrollContainer"
>
<div
class="flex justify-between divide-x divide-divider"
class="flex justify-between divide-x divide-dividerLight"
@wheel.prevent="scroll"
>
<div class="flex">
@@ -23,7 +23,8 @@
<template #item="{ element: [tabID, tabMeta] }">
<button
:key="`removable-tab-${tabID}`"
class="tab group px-2"
:id="`removable-tab-${tabID}`"
class="px-2 tab group"
:class="[{ active: modelValue === tabID }]"
:aria-label="tabMeta.label || ''"
role="button"
@@ -39,14 +40,14 @@
<div
v-if="!tabMeta.tabhead"
class="truncate w-full text-left px-2"
class="w-full px-2 text-left truncate"
>
<span class="truncate">
{{ tabMeta.label }}
</span>
</div>
<div v-else class="truncate w-full text-left">
<div v-else class="w-full text-left truncate">
<component :is="tabMeta.tabhead" />
</div>
@@ -72,7 +73,7 @@
},
'close',
]"
class="!p-0.25 rounded"
class="rounded !p-0.25"
@click.stop="emit('removeTab', tabID)"
/>
</button>
@@ -80,33 +81,33 @@
</draggable>
</div>
<div
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-8"
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-14"
>
<slot name="actions">
<span
v-if="canAddNewTab"
class="flex items-center justify-center px-3 bg-primaryLight z-8 h-full"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="newText ?? t?.('action.new') ?? 'New'"
:icon="IconPlus"
class="rounded !text-secondaryDark !p-1"
filled
@click="addTab"
/>
</span>
</slot>
<span
v-if="canAddNewTab"
class="flex items-center justify-center h-full px-3 bg-primaryLight z-8"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="newText ?? t?.('action.new') ?? 'New'"
:icon="IconPlus"
class="rounded create-new-tab !text-secondaryDark !p-1"
filled
@click="addTab"
/>
</span>
</div>
</div>
</div>
<slot name="actions" />
<input
type="range"
min="1"
:max="MAX_SCROLL_VALUE"
v-model="thumbPosition"
class="slider absolute bottom-0 hidden left-0"
class="absolute bottom-0 left-0 hidden slider"
:class="{
'!block': scrollThumb.show,
}"
@@ -131,7 +132,15 @@ import { pipe } from "fp-ts/function"
import { not } from "fp-ts/Predicate"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { ref, ComputedRef, computed, provide, inject, watch } from "vue"
import {
ref,
ComputedRef,
computed,
provide,
inject,
watch,
nextTick,
} from "vue"
import { useElementSize } from "@vueuse/core"
import type { Slot } from "vue"
import draggable from "vuedraggable-es"
@@ -186,9 +195,10 @@ const throwError = (message: string): never => {
throw new Error(message)
}
const TAB_WIDTH = 184
const tabEntries = ref<Array<[string, TabMeta]>>([])
const tabStyles = computed(() => ({
maxWidth: `${tabEntries.value.length * 184}px`,
maxWidth: `${tabEntries.value.length * TAB_WIDTH}px`,
width: "100%",
minWidth: "0px",
// transition: "max-width 0.2s",
@@ -292,6 +302,49 @@ watch(thumbPosition, (newVal) => {
const maxScroll = scrollWidth - clientWidth
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>
<style scoped lang="scss">
@@ -336,6 +389,13 @@ watch(thumbPosition, (newVal) => {
@apply text-secondaryDark;
@apply bg-primary;
@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 {
@@ -348,6 +408,16 @@ watch(thumbPosition, (newVal) => {
}
}
.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 {