Compare commits
7 Commits
fix/team-e
...
2023.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cce117b0a | ||
|
|
abc7b4b6f3 | ||
|
|
05e32ef9e4 | ||
|
|
f0a1fc319c | ||
|
|
385cabc6aa | ||
|
|
397b26a9f3 | ||
|
|
9a40058329 |
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.4.3",
|
||||
"version": "2023.4.4",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
}>(),
|
||||
{
|
||||
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
40
packages/hoppscotch-common/src/newstore/syncing.ts
Normal file
40
packages/hoppscotch-common/src/newstore/syncing.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/selfhost-web",
|
||||
"private": true,
|
||||
"version": "2023.4.3",
|
||||
"version": "2023.4.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:vite": "vite",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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
542
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user