Compare commits

..

2 Commits

Author SHA1 Message Date
Nivedin
37153b6401 feat: tabs UI in env selector 2023-05-11 17:47:06 +05:30
Nivedin
f3f9891017 feat: env selector change 2023-05-08 13:00:47 +05:30
30 changed files with 348 additions and 265 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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