feat: added inline environment variable edit button (#2813)
* refactor: changes v-if render to v-show on Environments tabs * feat: adds selectText prop to EnvInput * feat: adds editing variable name to env Details modal * feat: adds actions to invoke edit env modals * feat: adds edit action to tooltip env * refactor: adds destructuring assignment on action handlers for edit env modals * refactor: fix comment on environment modals action * chore: minor ui improvements * refactor: change text selecion prop on EnvInput to something more meaningful * refactor: removes comment on HoppEnvironment extension * refactor: renames isTextSelected EnvInput prop to selectTextOnMount * refactor: remove type definition of automatic inferrable variables * refactor: edit environment call to only allow accepted types * feat: introduce type safe action arguments * fix: revert v-show to v-if * chore: minor ui improvements Co-authored-by: Liyas Thomas <liyascthomas@gmail.com> Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
committed by
GitHub
parent
696c612489
commit
0069f51ea4
@@ -132,6 +132,7 @@ a {
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-6;
|
||||
@@ -166,10 +167,9 @@ a {
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply mr-1;
|
||||
@apply text-accentDark;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
@update-selected-team="updateSelectedTeam"
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsMy v-show="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsTeams
|
||||
v-else
|
||||
v-show="environmentType.type === 'team-environments'"
|
||||
:team="environmentType.selectedTeam"
|
||||
:team-environments="teamEnvironmentList"
|
||||
:loading="loading"
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-model="env.value"
|
||||
:select-text-on-mount="env.key === editingVariableName"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
@@ -163,6 +164,7 @@ const props = withDefaults(
|
||||
show: boolean
|
||||
action: "edit" | "new"
|
||||
editingEnvironmentIndex: number | "Global" | null
|
||||
editingVariableName: string | null
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
:show="showModalDetails"
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
@@ -83,6 +84,8 @@ import { useI18n } from "~/composables/i18n"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -100,6 +103,7 @@ const showModalImportExport = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<number | "Global" | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
@@ -122,4 +126,17 @@ const editEnvironment = (environmentIndex: number | "Global") => {
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
}
|
||||
)
|
||||
editEnvironment(envIndex >= 0 ? envIndex : "Global")
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-model="env.value"
|
||||
:select-text-on-mount="env.key === editingVariableName"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
@@ -173,6 +174,7 @@ const props = withDefaults(
|
||||
action: "edit" | "new"
|
||||
editingEnvironment: TeamEnvironment | null
|
||||
editingTeamId: string | undefined
|
||||
editingVariableName: string | null
|
||||
isViewer: boolean
|
||||
}>(),
|
||||
{
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
:action="action"
|
||||
:editing-environment="editingEnvironment"
|
||||
:editing-team-id="team?.id"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-viewer="team?.myRole === 'VIEWER'"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
@@ -125,6 +126,7 @@ import IconPlus from "~icons/lucide/plus"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { Team } from "~/helpers/backend/graphql"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -132,7 +134,7 @@ const colorMode = useColorMode()
|
||||
|
||||
type SelectedTeam = Team | undefined
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
team: SelectedTeam
|
||||
teamEnvironments: TeamEnvironment[]
|
||||
adapterError: GQLError<string> | null
|
||||
@@ -143,6 +145,7 @@ const showModalImportExport = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
@@ -178,4 +181,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
keymap,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import { EditorState, Extension } from "@codemirror/state"
|
||||
import { EditorSelection, EditorState, Extension } from "@codemirror/state"
|
||||
import { clone } from "lodash-es"
|
||||
import { history, historyKeymap } from "@codemirror/commands"
|
||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||
@@ -42,6 +42,7 @@ const props = withDefaults(
|
||||
styles?: string
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
readonly?: boolean
|
||||
}>(),
|
||||
{
|
||||
@@ -203,15 +204,28 @@ const initView = (el: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
const triggerTextSelection = () => {
|
||||
nextTick(() => {
|
||||
view.value?.focus()
|
||||
view.value?.dispatch({
|
||||
selection: EditorSelection.create([
|
||||
EditorSelection.range(0, props.modelValue.length),
|
||||
]),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
if (props.selectTextOnMount) triggerTextSelection()
|
||||
}
|
||||
})
|
||||
|
||||
watch(editor, () => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
if (props.selectTextOnMount) triggerTextSelection()
|
||||
} else {
|
||||
view.value?.destroy()
|
||||
view.value = undefined
|
||||
|
||||
@@ -22,6 +22,8 @@ export type HoppAction =
|
||||
| "modals.search.toggle" // Shows the search modal
|
||||
| "modals.support.toggle" // Shows the support modal
|
||||
| "modals.share.toggle" // Shows the share modal
|
||||
| "modals.my.environment.edit" // Edit current personal environment
|
||||
| "modals.team.environment.edit" // Edit current team environment
|
||||
| "navigation.jump.rest" // Jump to REST page
|
||||
| "navigation.jump.graphql" // Jump to GraphQL page
|
||||
| "navigation.jump.realtime" // Jump to realtime page
|
||||
@@ -36,31 +38,101 @@ export type HoppAction =
|
||||
| "response.file.download" // Download response as file
|
||||
| "response.copy" // Copy response to clipboard
|
||||
|
||||
/**
|
||||
* Defines the arguments, if present for a given type that is required to be passed on
|
||||
* invocation and will be passed to action handlers.
|
||||
*
|
||||
* This type is supposed to be an object with the key being one of the actions mentioned above.
|
||||
* The value to the key can be anything.
|
||||
* If an action has no argument, you do not need to add it to this type.
|
||||
*
|
||||
* NOTE: We can't enforce type checks to make sure the key is Action, you
|
||||
* will know if you got something wrong if there is a type error in this file
|
||||
*/
|
||||
type HoppActionArgs = {
|
||||
"modals.my.environment.edit": {
|
||||
envName: string
|
||||
variableName: string
|
||||
}
|
||||
"modals.team.environment.edit": {
|
||||
envName: string
|
||||
variableName: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HoppActions which require arguments for their invocation
|
||||
*/
|
||||
type HoppActionWithArgs = keyof HoppActionArgs
|
||||
|
||||
/**
|
||||
* HoppActions which do not require arguments for their invocation
|
||||
*/
|
||||
type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
||||
|
||||
/**
|
||||
* Resolves the argument type for a given HoppAction
|
||||
*/
|
||||
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
||||
? HoppActionArgs[A]
|
||||
: undefined
|
||||
|
||||
/**
|
||||
* Resolves the action function for a given HoppAction, used by action handler function defs
|
||||
*/
|
||||
type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
|
||||
? (arg: ArgOfHoppAction<A>) => void
|
||||
: () => void
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in HoppAction]?: Array<() => void>
|
||||
[A in HoppAction]?: Array<ActionFunc<A>>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = {}
|
||||
|
||||
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||
|
||||
export function bindAction(action: HoppAction, handler: () => void) {
|
||||
export function bindAction<A extends HoppAction>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
if (boundActions[action]) {
|
||||
boundActions[action]?.push(handler)
|
||||
} else {
|
||||
boundActions[action] = [handler]
|
||||
// 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
|
||||
boundActions[action] = [handler] as any
|
||||
}
|
||||
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function invokeAction(action: HoppAction) {
|
||||
boundActions[action]?.forEach((handler) => handler())
|
||||
type InvokeActionFunc = {
|
||||
(action: HoppActionWithNoArgs, args?: undefined): void
|
||||
<A extends HoppActionWithArgs>(action: A, args: ArgOfHoppAction<A>): void
|
||||
}
|
||||
|
||||
export function unbindAction(action: HoppAction, handler: () => void) {
|
||||
boundActions[action] = boundActions[action]?.filter((x) => x !== handler)
|
||||
/**
|
||||
* Invokes a action, triggering action handlers if any registered.
|
||||
* The second argument parameter is optional if your action has no args required
|
||||
* @param action The action to fire
|
||||
* @param args The argument passed to the action handler. Optional if action has no args required
|
||||
*/
|
||||
export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
|
||||
action: A,
|
||||
args: ArgOfHoppAction<A>
|
||||
) => {
|
||||
boundActions[action]?.forEach((handler) => handler(args!))
|
||||
}
|
||||
|
||||
export function unbindAction<A extends HoppAction>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
// 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
|
||||
boundActions[action] = boundActions[action]?.filter(
|
||||
(x) => x !== handler
|
||||
) as any
|
||||
|
||||
if (boundActions[action]?.length === 0) {
|
||||
delete boundActions[action]
|
||||
@@ -69,7 +141,10 @@ export function unbindAction(action: HoppAction, handler: () => void) {
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function defineActionHandler(action: HoppAction, handler: () => void) {
|
||||
export function defineActionHandler<A extends HoppAction>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
onMounted(() => {
|
||||
bindAction(action, handler)
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getAggregateEnvs,
|
||||
getSelectedEnvironmentType,
|
||||
} from "~/newstore/environments"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||
|
||||
@@ -58,17 +59,13 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
)
|
||||
return null
|
||||
|
||||
const envName =
|
||||
aggregateEnvs.find(
|
||||
(env) => env.key === text.slice(start - from, end - from)
|
||||
// env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
|
||||
)?.sourceEnv ?? "Choose an Environment"
|
||||
const parsedEnvKey = text.slice(start - from, end - from)
|
||||
|
||||
const envValue =
|
||||
aggregateEnvs.find(
|
||||
(env) => env.key === text.slice(start - from, end - from)
|
||||
// env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
|
||||
)?.value ?? "Not found"
|
||||
const tooltipEnv = aggregateEnvs.find((env) => env.key === parsedEnvKey)
|
||||
|
||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||
|
||||
const envValue = tooltipEnv?.value ?? "Not found"
|
||||
|
||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||
|
||||
@@ -76,9 +73,27 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
const selectedEnvType = getSelectedEnvironmentType()
|
||||
|
||||
const envTypeIcon = `<i class="inline-flex items-center pr-2 mr-2 -my-1 text-base border-r material-icons border-secondary">${
|
||||
const envTypeIcon = `<i class="inline-flex -my-1 -mx-0.5 opacity-65 items-center text-base material-icons border-secondary">${
|
||||
selectedEnvType === "TEAM_ENV" ? "people" : "person"
|
||||
}</i>`
|
||||
|
||||
const appendEditAction = (tooltip: HTMLElement) => {
|
||||
const editIcon = document.createElement("span")
|
||||
editIcon.className =
|
||||
"env-icon ml-2 text-accent cursor-pointer hover:text-accentDark"
|
||||
editIcon.addEventListener("click", () => {
|
||||
const isPersonalEnv =
|
||||
envName === "Global" || selectedEnvType !== "TEAM_ENV"
|
||||
const action = isPersonalEnv ? "my" : "team"
|
||||
invokeAction(`modals.${action}.environment.edit`, {
|
||||
envName,
|
||||
variableName: parsedEnvKey,
|
||||
})
|
||||
})
|
||||
editIcon.innerHTML = `<i class="inline-flex -my-1 -mx-1 items-center px-1 text-base material-icons border-secondary">drive_file_rename_outline</i>`
|
||||
tooltip.appendChild(editIcon)
|
||||
}
|
||||
|
||||
return {
|
||||
pos: start,
|
||||
end: to,
|
||||
@@ -90,11 +105,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
const kbd = document.createElement("kbd")
|
||||
const icon = document.createElement("span")
|
||||
icon.innerHTML = envTypeIcon
|
||||
icon.className = "env-icon"
|
||||
icon.className = "env-icon mr-2"
|
||||
kbd.textContent = finalEnv
|
||||
tooltipContainer.appendChild(icon)
|
||||
tooltipContainer.appendChild(document.createTextNode(`${envName} `))
|
||||
tooltipContainer.appendChild(kbd)
|
||||
if (tooltipEnv) appendEditAction(tooltipContainer)
|
||||
tooltipContainer.className = "tippy-content"
|
||||
dom.className = "tippy-box"
|
||||
dom.dataset.theme = "tooltip"
|
||||
|
||||
@@ -102,11 +102,14 @@ export const baseTheme = EditorView.theme({
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
".cm-tooltip-arrow": {
|
||||
color: "var(--tooltip-color)",
|
||||
},
|
||||
".cm-tooltip-arrow:after": {
|
||||
borderTopColor: "var(--tooltip-color) !important",
|
||||
borderTopColor: "inherit !important",
|
||||
},
|
||||
".cm-tooltip-arrow:before": {
|
||||
borderTopColor: "var(--tooltip-color) !important",
|
||||
borderTopColor: "inherit !important",
|
||||
},
|
||||
".cm-tooltip.cm-tooltip-autocomplete > ul": {
|
||||
fontFamily: "var(--font-mono)",
|
||||
@@ -234,11 +237,14 @@ export const inputTheme = EditorView.theme({
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
".cm-tooltip-arrow": {
|
||||
color: "var(--tooltip-color)",
|
||||
},
|
||||
".cm-tooltip-arrow:after": {
|
||||
borderTopColor: "var(--tooltip-color) !important",
|
||||
borderTopColor: "currentColor !important",
|
||||
},
|
||||
".cm-tooltip-arrow:before": {
|
||||
borderTopColor: "var(--tooltip-color) !important",
|
||||
borderTopColor: "currentColor !important",
|
||||
},
|
||||
".cm-tooltip.cm-tooltip-autocomplete > ul": {
|
||||
fontFamily: "var(--font-mono)",
|
||||
|
||||
Reference in New Issue
Block a user