diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index d3920da8..ba4643c7 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -68,7 +68,6 @@
"turn_on": "Turn on",
"undo": "Undo",
"yes": "Yes",
- "confirm": "Confirm",
"verify": "Verify",
"enable": "Enable",
"disable": "Disable"
@@ -367,6 +366,7 @@
"protocols": "Protocols are empty",
"request_variables": "This request does not have any request variables",
"schema": "Connect to a GraphQL endpoint to view schema",
+ "search_environment": "No matching environment found for",
"secret_environments": "Secrets are not synced to Hoppscotch",
"shared_requests": "Shared requests are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one",
diff --git a/packages/hoppscotch-common/src/components/environments/Selector.vue b/packages/hoppscotch-common/src/components/environments/Selector.vue
index d0e511cd..0ee4c8c8 100644
--- a/packages/hoppscotch-common/src/components/environments/Selector.vue
+++ b/packages/hoppscotch-common/src/components/environments/Selector.vue
@@ -26,12 +26,20 @@
+
+ v-if="filteredAndAlphabetizedPersonalEnvs.length === 0"
+ class="break-words"
+ :src="
+ filterText
+ ? undefined
+ : `/images/states/${colorMode.value}/blockchain.svg`
+ "
+ :alt="
+ filterText
+ ? `${t('empty.search_environment')}`
+ : t('empty.environments')
+ "
+ :text="
+ filterText
+ ? `${t('empty.search_environment')} '${filterText}'`
+ : t('empty.environments')
+ "
+ >
+
+
+
+
+
+ v-if="filteredAndAlphabetizedTeamEnvs.length === 0"
+ class="break-words"
+ :src="
+ filteredAndAlphabetizedTeamEnvs.length === 0 && !filterText
+ ? `/images/states/${colorMode.value}/blockchain.svg`
+ : undefined
+ "
+ :alt="
+ filterText
+ ? `${t('empty.search_environment')}`
+ : t('empty.environments')
+ "
+ :text="
+ filterText
+ ? `${t('empty.search_environment')} '${filterText}'`
+ : t('empty.environments')
+ "
+ >
+
+
+
+
- sortPersonalEnvironmentsAlphabetically(myEnvironments.value, "asc")
-)
+// Sort environments alphabetically by default and filter based on search
+const filteredAndAlphabetizedPersonalEnvs = computed(() => {
+ const envs = sortPersonalEnvironmentsAlphabetically(
+ myEnvironments.value,
+ "asc"
+ )
-const alphabeticallySortedTeamEnvironments = computed(() =>
- sortTeamEnvironmentsAlphabetically(teamEnvironmentList.value, "asc")
-)
+ if (selectedEnvTab.value !== "my-environments" || !filterText.value)
+ return envs
+
+ // Ensure specifying whitespace characters alone result in the empty state for no search results
+ const trimmedFilterText = filterText.value.trim().toLowerCase()
+
+ return envs.filter(({ env }) =>
+ trimmedFilterText
+ ? env.name.toLowerCase().includes(trimmedFilterText)
+ : false
+ )
+})
+
+const filteredAndAlphabetizedTeamEnvs = computed(() => {
+ const envs = sortTeamEnvironmentsAlphabetically(
+ teamEnvironmentList.value,
+ "asc"
+ )
+
+ if (selectedEnvTab.value !== "team-environments" || !filterText.value)
+ return envs
+
+ // Ensure specifying whitespace characters alone result in the empty state for no search results
+ const trimmedFilterText = filterText.value.trim().toLowerCase()
+
+ return envs.filter(({ env }) =>
+ trimmedFilterText
+ ? env.environment.name.toLowerCase().includes(trimmedFilterText)
+ : false
+ )
+})
const handleEnvironmentChange = (
index: number,
@@ -603,6 +675,14 @@ const editGlobalEnv = () => {
invokeAction("modals.global.environment.update", {})
}
+// Filter input disabled if no environments are available
+const isFilterInputDisabled = computed(() => {
+ if (selectedEnvTab.value === "my-environments") {
+ return myEnvironments.value.length === 0
+ }
+ return teamEnvironmentList.value.length === 0
+})
+
const editEnv = () => {
if (selectedEnv.value.type === "MY_ENV" && selectedEnv.value.name) {
invokeAction("modals.my.environment.edit", {
diff --git a/packages/hoppscotch-common/src/components/environments/my/index.vue b/packages/hoppscotch-common/src/components/environments/my/index.vue
index 94a16761..0672fe33 100644
--- a/packages/hoppscotch-common/src/components/environments/my/index.vue
+++ b/packages/hoppscotch-common/src/components/environments/my/index.vue
@@ -1,5 +1,13 @@
+
-
+
+
+
+
+
{{ t("environment.import_or_create") }}
@@ -106,10 +131,27 @@ const emit = defineEmits<{
const environments = useReadonlyStream(environments$, [])
-// Sort environments alphabetically by default
-const alphabeticallySortedPersonalEnvironments = computed(() =>
- sortPersonalEnvironmentsAlphabetically(environments.value, "asc")
-)
+const filterText = ref("")
+
+// Sort environments alphabetically by default and filter by search text
+const filteredAndAlphabetizedPersonalEnvs = computed(() => {
+ const envs = sortPersonalEnvironmentsAlphabetically(environments.value, "asc")
+ const rawFilter = filterText.value
+
+ // Ensure specifying whitespace characters alone result in the empty state for no search results
+ const trimmedFilter = rawFilter.trim().toLowerCase()
+
+ // Whitespace-only input results in an empty state
+ if (rawFilter && !trimmedFilter) return []
+
+ // No search text → Show all environments
+ if (!trimmedFilter) return envs
+
+ // Filter environments based on search text
+ return envs.filter(({ env }) =>
+ env.name.toLowerCase().includes(trimmedFilter)
+ )
+})
const showModalImportExport = ref(false)
const showModalDetails = ref(false)
@@ -166,7 +208,7 @@ defineActionHandler(
"modals.my.environment.edit",
({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName
- const env = alphabeticallySortedPersonalEnvironments.value.find(
+ const env = filteredAndAlphabetizedPersonalEnvs.value.find(
({ env }) => env.name === envName
)
if (envName !== "Global" && env) {
diff --git a/packages/hoppscotch-common/src/components/environments/teams/index.vue b/packages/hoppscotch-common/src/components/environments/teams/index.vue
index 346d5b1b..eb1d32ce 100644
--- a/packages/hoppscotch-common/src/components/environments/teams/index.vue
+++ b/packages/hoppscotch-common/src/components/environments/teams/index.vue
@@ -1,5 +1,13 @@
+
+
+
+ {{ t("state.loading") }}
+
+
+
+
+ {{ t(getEnvActionErrorMessage(adapterError)) }}
+
+
-
+
+
+
+
+
{{ t("environment.import_or_create") }}
@@ -81,10 +112,11 @@
-
+
+
-
-
- {{ t("state.loading") }}
-
-
-
- {{ t(getEnvActionErrorMessage(adapterError)) }}
-
+
()
-// Sort environments alphabetically by default
+const filterText = ref("")
-const alphabeticallySortedTeamEnvironments = computed(() =>
- sortTeamEnvironmentsAlphabetically(props.teamEnvironments, "asc")
-)
+// Sort environments alphabetically by default and filter by search text
+const filteredAndAlphabetizedTeamEnvs = computed(() => {
+ const envs = sortTeamEnvironmentsAlphabetically(props.teamEnvironments, "asc")
+ const rawFilter = filterText.value
+
+ // Ensure specifying whitespace characters alone result in the empty state for no search results
+ const trimmedFilter = rawFilter.trim().toLowerCase()
+
+ // Whitespace-only input results in an empty state
+ if (rawFilter && !trimmedFilter) return []
+
+ // No search text → Show all environments
+ if (!trimmedFilter) return envs
+
+ // Filter environments based on search text
+ return envs.filter(({ env }) =>
+ env.environment.name.toLowerCase().includes(trimmedFilter)
+ )
+})
const showModalImportExport = ref(false)
const showModalDetails = ref(false)
@@ -241,7 +277,7 @@ defineActionHandler(
"modals.team.environment.edit",
({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName
- const teamEnvToEdit = alphabeticallySortedTeamEnvironments.value.find(
+ const teamEnvToEdit = filteredAndAlphabetizedTeamEnvs.value.find(
({ env }) => env.environment.name === envName
)
if (teamEnvToEdit) {
diff --git a/packages/hoppscotch-common/src/components/smart/EnvInput.vue b/packages/hoppscotch-common/src/components/smart/EnvInput.vue
index 32a720a7..946ce581 100644
--- a/packages/hoppscotch-common/src/components/smart/EnvInput.vue
+++ b/packages/hoppscotch-common/src/components/smart/EnvInput.vue
@@ -73,7 +73,12 @@ import {
keymap,
tooltips,
} from "@codemirror/view"
-import { EditorSelection, EditorState, Extension } from "@codemirror/state"
+import {
+ Compartment,
+ EditorSelection,
+ EditorState,
+ Extension,
+} from "@codemirror/state"
import { clone } from "lodash-es"
import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
@@ -156,6 +161,9 @@ const isSecret = ref(props.secret)
const secretText = ref(props.modelValue)
+// Compartment to store the readOnly state of the editor
+const readOnly = new Compartment()
+
watch(
() => secretText.value,
(newVal) => {
@@ -478,16 +486,14 @@ const initView = (el: any) => {
}
const getExtensions = (readonly: boolean): Extension => {
- const extensions: Extension = [
- EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
- EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
+ const readOnlyConfigs = [
EditorView.updateListener.of((update) => {
if (readonly) {
update.view.contentDOM.inputMode = "none"
+ update.view.contentDOM.contentEditable = "false"
}
}),
EditorState.changeFilter.of(() => !readonly),
- inputTheme,
readonly
? EditorView.theme({
".cm-content": {
@@ -498,6 +504,13 @@ const getExtensions = (readonly: boolean): Extension => {
},
})
: EditorView.theme({}),
+ ]
+
+ const extensions: Extension = [
+ EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
+ EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
+ inputTheme,
+ readOnly.of(readOnlyConfigs),
tooltips({
parent: document.body,
position: "absolute",
@@ -529,7 +542,8 @@ const getExtensions = (readonly: boolean): Extension => {
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {
- if (readonly) return
+ // since the readonly prop is reactive, we need to check if it has changed
+ if (props.readonly) return
if (update.docChanged) {
const prevValue = clone(cachedValue.value)
@@ -610,6 +624,51 @@ watch(editor, () => {
view.value = undefined
}
})
+
+/**
+ * Watch for changes in the readonly prop
+ * and update the editor state accordingly
+ */
+watch(
+ () => props.readonly,
+ (isReadOnly) => {
+ if (isReadOnly) {
+ view.value?.dispatch({
+ effects: readOnly.reconfigure([
+ EditorView.theme({
+ ".cm-content": {
+ caretColor: "var(--secondary-dark-color)",
+ color: "var(--secondary-dark-color)",
+ backgroundColor: "var(--divider-color)",
+ opacity: 0.25,
+ },
+ }),
+ EditorState.changeFilter.of(() => false),
+ EditorState.readOnly.of(true),
+ ]),
+ })
+
+ //change input mode and contenteditable to false to prevent keyboard input
+ view.value?.contentDOM.setAttribute("inputmode", "none")
+ view.value?.contentDOM.setAttribute("contenteditable", "false")
+ } else {
+ view.value?.dispatch({
+ effects: readOnly.reconfigure([
+ EditorView.theme({}),
+ EditorState.changeFilter.of(() => true),
+ EditorState.readOnly.of(false),
+ ]),
+ })
+
+ //change input mode and contenteditable to true to allow keyboard input
+ view.value?.contentDOM.setAttribute("inputmode", "text")
+ view.value?.contentDOM.setAttribute("contenteditable", "true")
+ }
+ },
+ {
+ immediate: true,
+ }
+)