Compare commits

...

7 Commits

Author SHA1 Message Date
Andrew Bastin
1cce117b0a chore: bump version to 2023.4.4 2023-06-02 11:06:51 -04:00
Liyas Thomas
abc7b4b6f3 chore: improve mobile responsiveness on environment selector (#3100) 2023-06-02 10:56:18 -04:00
Ankit Sridhar
05e32ef9e4 fix: update team invitation link to domain specified in .env [HBE-202] (#3096) 2023-05-31 10:36:34 -04:00
Nivedin
f0a1fc319c fix: sync popup firing multiple times (#3063) 2023-05-30 23:36:37 -04:00
Allen Zhang
385cabc6aa fix: update package.json script (#3083) 2023-05-30 17:50:47 -04:00
Liyas Thomas
397b26a9f3 chore: environment selector with new ux (#3052)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-05-30 17:47:37 -04:00
Nivedin
9a40058329 fix: set team environment from test (#3059) 2023-05-30 17:38:28 -04:00
17 changed files with 728 additions and 285 deletions

View File

@@ -11,7 +11,7 @@
"dev": "pnpm -r do-dev",
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
"generate": "pnpm -r do-build-prod",
"start": "http-server packages/hoppscotch-web/dist -p 3000",
"start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
"lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix",

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.3",
"version": "2023.4.4",
"description": "",
"author": "",
"private": true,

View File

@@ -126,7 +126,7 @@ export class TeamInvitationService {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`,
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
invite_team_name: team.name,
},
}),

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.4.3",
"version": "2023.4.4",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"dev:vite": "vite",

View File

@@ -126,7 +126,7 @@
/>
<CollectionsEditRequest
:show="showModalEditRequest"
v-bind:model-value="editingRequest ? editingRequest.name : ''"
:model-value="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState"
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"

View File

@@ -11,13 +11,14 @@
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')}`"
:icon="IconLayers"
:label="
mdAndLarger
? selectedEnv.type !== 'NO_ENV_SELECTED'
? selectedEnv.name
: `${t('environment.select')}`
: ''
"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
@@ -46,93 +47,145 @@
}
"
/>
<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"
<HoppSmartTabs
v-model="selectedEnvTab"
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary"
render-inactive-tabs
>
<HoppSmartTab
:id="'my-environments'"
:label="`${t('environment.my_environments')}`"
>
<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"
v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
hide()
}
"
/>
</div>
<div
v-if="!teamEnvLoading && isAdapterError"
class="flex flex-col items-center py-4"
<div
v-if="myEnvironments.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ errorMessage }}
</div>
</div>
<div
v-if="teamListLoading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
: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
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
</template>
</tippy>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue"
import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
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 { useReadonlyStream, useStream } from "~/composables/stream"
import {
environments$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const t = useI18n()
const colorMode = useColorMode()
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 myEnvironments = useReadonlyStream(environments$, [])
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
const teamEnvironmentList = useReadonlyStream(
teamEnvListAdapter.teamEnvironmentList$,
[]
)
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
@@ -140,15 +193,35 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex
)
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: props.myEnvironments[selectedEnvironmentIndex.value.index].name,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = props.teamEnvironmentList.find(
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
@@ -170,4 +243,17 @@ const selectedEnv = computed(() => {
// Template refs
const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script>

View File

@@ -4,15 +4,6 @@
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
>
<WorkspaceCurrent :section="t('tab.environments')" />
<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"
@@ -46,13 +37,11 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n"
import {
environments$,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -147,24 +136,19 @@ onLoggedIn(() => {
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// 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
// If there is no teamID, switch to my environment
// Switch to my environments if workspace is personal and to team environments if workspace is team
// also resets selected environment if workspace is personal and the previous selected environment was a team environment
watch(workspace, (newWorkspace) => {
if (newWorkspace.type === "personal") {
switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
}
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
}
})
@@ -207,8 +191,6 @@ defineActionHandler(
}
)
const myEnvironments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
@@ -251,17 +233,4 @@ watch(
},
{ deep: true }
)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script>

View File

@@ -165,8 +165,8 @@ const props = withDefaults(
defineProps<{
show: boolean
action: "edit" | "new"
editingEnvironmentIndex: number | "Global" | null
editingVariableName: string | null
editingEnvironmentIndex?: number | "Global" | null
editingVariableName?: string | null
envVars?: () => Environment["variables"]
}>(),
{

View File

@@ -140,7 +140,7 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function"
import { parseTemplateStringE } from "@hoppscotch/data"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { clone } from "lodash-es"
import { useToast } from "@composables/toast"
@@ -173,16 +173,20 @@ const props = withDefaults(
defineProps<{
show: boolean
action: "edit" | "new"
editingEnvironment: TeamEnvironment | null
editingEnvironment?: TeamEnvironment | null
editingTeamId: string | undefined
editingVariableName: string | null
isViewer: boolean
editingVariableName?: string | null
isViewer?: boolean
envVars?: () => Environment["variables"]
}>(),
{
show: false,
action: "edit",
editingEnvironment: null,
editingTeamId: "",
editingVariableName: null,
isViewer: false,
envVars: () => [],
}
)
@@ -226,10 +230,16 @@ watch(
() => props.show,
(show) => {
if (show) {
if (props.editingEnvironment === null) {
if (props.action === "new") {
name.value = null
vars.value = []
} else {
vars.value = pipe(
props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({
id: idTicker.value++,
env: clone(e),
}))
)
} else if (props.editingEnvironment !== null) {
name.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe(
props.editingEnvironment.environment.variables ?? [],

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary"
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
>
<HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'"

View File

@@ -197,11 +197,20 @@
/>
</div>
<EnvironmentsMyDetails
:show="showModalDetails"
:show="showMyEnvironmentDetailsModal"
action="new"
:env-vars="getAdditionVars"
@hide-modal="displayModalAdd(false)"
/>
<EnvironmentsTeamsDetails
:show="showTeamEnvironmentDetailsModal"
action="new"
:env-vars="getAdditionVars"
:editing-team-id="
workspace.type === 'team' ? workspace.teamID : undefined
"
@hide-modal="displayModalAdd(false)"
/>
</div>
</template>
@@ -225,6 +234,7 @@ import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core"
import { workspaceStatus$ } from "~/newstore/workspace"
const props = defineProps<{
modelValue: HoppTestResult | null | undefined
@@ -239,10 +249,15 @@ const testResults = useVModel(props, "modelValue", emit)
const t = useI18n()
const colorMode = useColorMode()
const showModalDetails = ref(false)
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const showMyEnvironmentDetailsModal = ref(false)
const showTeamEnvironmentDetailsModal = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => {
showModalDetails.value = shouldDisplay
if (workspace.value.type === "personal")
showMyEnvironmentDetailsModal.value = shouldDisplay
else showTeamEnvironmentDetailsModal.value = shouldDisplay
}
/**

View File

@@ -0,0 +1,40 @@
import { distinctUntilChanged, pluck } from "rxjs"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
type SyncState = {
isInitialSync: boolean
shouldSync: boolean
}
type CurrentSyncingState = {
currentSyncingItem: SyncState
}
const initialState: CurrentSyncingState = {
currentSyncingItem: {
isInitialSync: false,
shouldSync: false,
},
}
const dispatchers = defineDispatchers({
changeCurrentSyncStatus(_, { syncItem }: { syncItem: SyncState }) {
return {
currentSyncingItem: syncItem,
}
},
})
export const currentSyncStore = new DispatchingStore(initialState, dispatchers)
export const currentSyncingStatus$ = currentSyncStore.subject$.pipe(
pluck("currentSyncingItem"),
distinctUntilChanged()
)
export function changeCurrentSyncStatus(syncItem: SyncState) {
currentSyncStore.dispatch({
dispatcher: "changeCurrentSyncStatus",
payload: { syncItem },
})
}

View File

@@ -56,6 +56,9 @@
@update:model-value="onTabUpdate"
/>
</HoppSmartWindow>
<template #actions>
<EnvironmentsSelector class="h-full" />
</template>
</HoppSmartWindows>
</template>
<template #sidebar>
@@ -63,8 +66,8 @@
</template>
</AppPaneLayout>
<CollectionsEditRequest
:show="showRenamingReqNameModal"
v-model="reqName"
:show="showRenamingReqNameModal"
@submit="renameReqName"
@hide-modal="showRenamingReqNameModal = false"
/>
@@ -84,7 +87,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, onBeforeMount } from "vue"
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
@@ -120,6 +123,11 @@ import { useToast } from "~/composables/toast"
import { PersistableRESTTabState } from "~/helpers/rest/tab"
import { watchDebounced } from "@vueuse/core"
import { oauthRedirect } from "~/helpers/oauth"
import { useReadonlyStream } from "~/composables/stream"
import {
changeCurrentSyncStatus,
currentSyncingStatus$,
} from "~/newstore/syncing"
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
@@ -131,7 +139,10 @@ const toast = useToast()
const tabs = getActiveTabs()
const confirmSync = ref(false)
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
isInitialSync: false,
shouldSync: true,
})
const tabStateForSync = ref<PersistableRESTTabState | null>(null)
function bindRequestToURLParams() {
@@ -226,29 +237,6 @@ const onSaveModalClose = () => {
}
}
watch(confirmSync, (newValue) => {
if (newValue) {
toast.show(t("confirm.sync"), {
duration: 0,
action: [
{
text: `${t("action.yes")}`,
onClick: (_, toastObject) => {
syncTabState()
toastObject.goAway(0)
},
},
{
text: `${t("action.no")}`,
onClick: (_, toastObject) => {
toastObject.goAway(0)
},
},
],
})
}
})
const syncTabState = () => {
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
}
@@ -287,6 +275,35 @@ function startTabStateSync(): Subscription {
return sub
}
const showSyncToast = () => {
toast.show(t("confirm.sync"), {
duration: 0,
action: [
{
text: `${t("action.yes")}`,
onClick: (_, toastObject) => {
syncTabState()
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: true,
})
toastObject.goAway(0)
},
},
{
text: `${t("action.no")}`,
onClick: (_, toastObject) => {
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: false,
})
toastObject.goAway(0)
},
},
],
})
}
function setupTabStateSync() {
const route = useRoute()
@@ -302,9 +319,15 @@ function setupTabStateSync() {
const tabStateFromSync =
await platform.sync.tabState.loadTabStateFromSync()
if (tabStateFromSync) {
if (tabStateFromSync && !confirmSync.value.isInitialSync) {
tabStateForSync.value = tabStateFromSync
confirmSync.value = true
showSyncToast()
// Have to set isInitialSync to true here because the toast is shown
// and the user does not click on any of the actions
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: false,
})
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/selfhost-web",
"private": true,
"version": "2023.4.3",
"version": "2023.4.4",
"type": "module",
"scripts": {
"dev:vite": "vite",

View File

@@ -1,7 +1,7 @@
{
"name": "hoppscotch-sh-admin",
"private": true,
"version": "2023.4.3",
"version": "2023.4.4",
"type": "module",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",

View File

@@ -100,7 +100,9 @@
</div>
</div>
<slot name="actions" />
<div v-if="hasActions" :class="mdAndLarger ? 'w-64' : 'w-16'">
<slot name="actions" />
</div>
<input
type="range"
@@ -111,10 +113,10 @@
:class="{
'!block': scrollThumb.show,
}"
:style="{
'--thumb-width': scrollThumb.width + 'px',
}"
style="width: calc(100% - 3rem)"
:style="[
`--thumb-width: ${scrollThumb.width}px`,
`width: calc(100% - ${hasActions ? mdAndLarger ? '19rem' : '7rem' : '3rem'})`,
]"
id="myRange"
/>
</div>
@@ -140,8 +142,9 @@ import {
inject,
watch,
nextTick,
useSlots,
} from "vue"
import { useElementSize } from "@vueuse/core"
import { breakpointsTailwind, useBreakpoints, useElementSize } from "@vueuse/core"
import type { Slot } from "vue"
import draggable from "vuedraggable-es"
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
@@ -164,6 +167,9 @@ export type TabProvider = {
removeTabEntry: (tabID: string) => void
}
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = withDefaults(
@@ -191,6 +197,12 @@ const emit = defineEmits<{
(e: "addTab"): void
}>()
const slots = useSlots()
const hasActions = computed(() => {
return !!slots.actions
})
const throwError = (message: string): never => {
throw new Error(message)
}

542
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff