Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ exports[`GeneralSetting should match snapshot 1`] = `
class="input pb-0 pr-2"
>
<label
class="input__label translate-y-2 text-sm"
class="input__label pointer-events-none translate-y-2 text-sm"
for="Description (optional)"
>
Description (optional)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { type VariableResponse } from 'qovery-typescript-axios'
import { type ReactNode } from 'react'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { CreateUpdateVariableModal } from './create-update-variable-modal'

jest.mock('@qovery/shared/ui', () => {
const actual = jest.requireActual('@qovery/shared/ui')

return {
...actual,
useModal: () => ({
enableAlertClickOutside: jest.fn(),
}),
}
})

jest.mock('../dropdown-variable/dropdown-variable', () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => children,
}))

jest.mock('./variable-value-editor-modal/variable-value-editor-modal', () => ({
VariableValueEditorModal: ({ open }: { open: boolean }) =>
open ? <div data-testid="mock-value-editor-modal" /> : null,
}))

jest.mock('../hooks/use-create-variable/use-create-variable', () => ({
useCreateVariable: () => ({
mutateAsync: jest.fn(),
}),
}))

jest.mock('../hooks/use-create-variable-alias/use-create-variable-alias', () => ({
useCreateVariableAlias: () => ({
mutateAsync: jest.fn(),
}),
}))

jest.mock('../hooks/use-create-variable-override/use-create-variable-override', () => ({
useCreateVariableOverride: () => ({
mutateAsync: jest.fn(),
}),
}))

jest.mock('../hooks/use-edit-variable/use-edit-variable', () => ({
useEditVariable: () => ({
mutateAsync: jest.fn(),
}),
}))

const closeModal = jest.fn()

const baseProps = {
closeModal,
scope: 'APPLICATION' as const,
projectId: 'project-id',
environmentId: 'environment-id',
serviceId: 'service-id',
}

const baseVariable = {
id: 'variable-id',
created_at: '2024-04-10T09:56:19.908145Z',
updated_at: '2024-04-10T09:56:19.908145Z',
key: 'MY_VARIABLE',
value: 'initial value',
mount_path: null,
scope: 'APPLICATION',
overridden_variable: null,
aliased_variable: null,
variable_type: 'VALUE',
variable_kind: 'Public',
service_id: 'service-id',
service_name: 'service-name',
service_type: 'APPLICATION',
owned_by: 'QOVERY',
is_secret: false,
} as VariableResponse

describe('CreateUpdateVariableModal', () => {
beforeEach(() => {
closeModal.mockReset()
})

it('should render the open editor button when the value field is available', () => {
renderWithProviders(<CreateUpdateVariableModal {...baseProps} mode="CREATE" type="VALUE" />)

expect(screen.getByRole('button', { name: /open editor/i })).toBeInTheDocument()
})

it('should not render the open editor button for aliases', () => {
renderWithProviders(<CreateUpdateVariableModal {...baseProps} mode="CREATE" type="ALIAS" variable={baseVariable} />)

expect(screen.queryByRole('button', { name: /open editor/i })).not.toBeInTheDocument()
})

it('should open the fullscreen editor modal when clicking the button', async () => {
const { userEvent } = renderWithProviders(
<CreateUpdateVariableModal {...baseProps} mode="UPDATE" type="VALUE" variable={baseVariable} />
)

await userEvent.click(screen.getByRole('button', { name: /open editor/i }))

expect(screen.getByTestId('mock-value-editor-modal')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,32 @@ import { useCreateVariableAlias } from '../hooks/use-create-variable-alias/use-c
import { useCreateVariableOverride } from '../hooks/use-create-variable-override/use-create-variable-override'
import { useCreateVariable } from '../hooks/use-create-variable/use-create-variable'
import { useEditVariable } from '../hooks/use-edit-variable/use-edit-variable'
import { VariableValueEditorModal } from './variable-value-editor-modal/variable-value-editor-modal'

type Scope = Exclude<keyof typeof APIVariableScopeEnum, 'BUILT_IN'>
type ValueEditorScope = Extract<Scope, 'APPLICATION' | 'CONTAINER' | 'JOB' | 'HELM' | 'TERRAFORM'>

function isValueEditorScope(scope: Scope | undefined): scope is ValueEditorScope {
return ['APPLICATION', 'CONTAINER', 'JOB', 'HELM', 'TERRAFORM'].includes(scope ?? '')
}

function getValueEditorLanguage({ isFile, mountPath }: { isFile: boolean; mountPath?: string }) {
if (!isFile || !mountPath) {
return 'plaintext'
}

const normalizedMountPath = mountPath.toLowerCase()

if (normalizedMountPath.endsWith('.yaml') || normalizedMountPath.endsWith('.yml')) {
return 'yaml'
}

if (normalizedMountPath.endsWith('.json')) {
return 'json'
}

return 'plaintext'
}

export type CreateUpdateVariableModalProps = {
closeModal: () => void
Expand Down Expand Up @@ -58,6 +82,7 @@ export function CreateUpdateVariableModal(props: CreateUpdateVariableModalProps)
const _isFile = (variable && environmentVariableFile(variable)) || (isFile ?? false)
const { enableAlertClickOutside } = useModal()
const [loading, setLoading] = useState(false)
const [isValueEditorOpen, setIsValueEditorOpen] = useState(false)

const { mutateAsync: createVariable } = useCreateVariable()
const { mutateAsync: createVariableAlias } = useCreateVariableAlias()
Expand Down Expand Up @@ -128,6 +153,10 @@ export function CreateUpdateVariableModal(props: CreateUpdateVariableModalProps)

methods.watch(() => enableAlertClickOutside(methods.formState.isDirty))
const watchScope = methods.watch('scope')
const watchMountPath = methods.watch('mountPath')
const valueEditorLanguage = getValueEditorLanguage({ isFile: _isFile, mountPath: watchMountPath })
const valueEditorServiceId = 'serviceId' in props && isValueEditorScope(watchScope) ? props.serviceId : undefined
const valueEditorScope = isValueEditorScope(watchScope) ? watchScope : undefined

const _onSubmit = methods.handleSubmit(async (data) => {
const cloneData = { ...data }
Expand Down Expand Up @@ -375,33 +404,59 @@ export function CreateUpdateVariableModal(props: CreateUpdateVariableModalProps)
name="value"
control={methods.control}
render={({ field: { name, onChange, value }, fieldState: { error } }) => (
<div className="relative">
<InputTextArea
ref={textareaRef}
className="mb-3"
name={name}
onChange={onChange}
value={value}
label="Value"
error={error?.message}
/>
{'environmentId' in props && (
<DropdownVariable
environmentId={props.environmentId}
onChange={(variableKey) => handleInsertVariable({ variableKey, value: value || '', onChange })}
<>
<div className="mb-2 flex justify-end">
<Button
type="button"
size="sm"
variant="outline"
className="gap-1.5"
onClick={() => setIsValueEditorOpen(true)}
>
<Button
size="md"
type="button"
color="neutral"
variant="surface"
className="absolute bottom-1.5 right-1.5 w-8 justify-center"
<Icon iconName="arrows-maximize" iconStyle="regular" className="text-xs" />
Open editor
</Button>
</div>
<div className="relative">
<InputTextArea
ref={textareaRef}
className="mb-3"
name={name}
onChange={onChange}
value={value}
label="Value"
error={error?.message}
/>
{'environmentId' in props && (
<DropdownVariable
environmentId={props.environmentId}
onChange={(variableKey) => handleInsertVariable({ variableKey, value: value || '', onChange })}
>
<Icon className="text-sm" iconName="wand-magic-sparkles" />
</Button>
</DropdownVariable>
)}
</div>
<Button
size="md"
type="button"
color="neutral"
variant="surface"
className="absolute bottom-1.5 right-1.5 w-8 justify-center"
>
<Icon className="text-sm" iconName="wand-magic-sparkles" />
</Button>
</DropdownVariable>
)}
</div>
<VariableValueEditorModal
open={isValueEditorOpen}
onOpenChange={setIsValueEditorOpen}
value={value}
onSave={onChange}
title="Value editor"
description="Edit the value in a larger editor."
language={valueEditorLanguage}
environmentId={'environmentId' in props ? props.environmentId : undefined}
serviceId={valueEditorServiceId}
scope={valueEditorScope}
/>
</>
)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { renderWithProviders, screen, within } from '@qovery/shared/util-tests'
import { VariableValueEditorModal } from './variable-value-editor-modal'

jest.mock('@qovery/shared/ui', () => {
const actual = jest.requireActual('@qovery/shared/ui')

return {
...actual,
CodeEditor: ({ value, onChange }: { value?: string | null; onChange?: (value: string) => void }) => (
<textarea
data-testid="mock-code-editor"
value={value ?? ''}
onChange={(event) => onChange?.(event.target.value)}
/>
),
}
})

jest.mock('../../code-editor-variable/code-editor-variable', () => ({
CodeEditorVariable: ({ value, onChange }: { value?: string | null; onChange: (value: string) => void }) => (
<textarea
data-testid="mock-code-editor-variable"
value={value ?? ''}
onChange={(event) => onChange(event.target.value)}
/>
),
}))

describe('VariableValueEditorModal', () => {
it('should save the edited value and close the modal', async () => {
const onSave = jest.fn()
const onOpenChange = jest.fn()
const { userEvent } = renderWithProviders(
<VariableValueEditorModal
open={true}
onOpenChange={onOpenChange}
onSave={onSave}
value="initial value"
title="Value editor"
description="Edit the value in a larger editor."
language="plaintext"
environmentId="environment-id"
/>
)

await screen.findByTestId('submit-button')
await userEvent.clear(screen.getByTestId('mock-code-editor-variable'))
await userEvent.type(screen.getByTestId('mock-code-editor-variable'), 'updated value')

const fullscreenEditor = screen.getByTestId('value-full-screen-editor')
await userEvent.click(within(fullscreenEditor).getByTestId('submit-button'))

expect(onSave).toHaveBeenCalledWith('updated value')
expect(onOpenChange).toHaveBeenCalledWith(false)
})

it('should close without saving when cancelling', async () => {
const onSave = jest.fn()
const onOpenChange = jest.fn()
const { userEvent } = renderWithProviders(
<VariableValueEditorModal
open={true}
onOpenChange={onOpenChange}
onSave={onSave}
value="initial value"
title="Value editor"
description="Edit the value in a larger editor."
language="plaintext"
environmentId="environment-id"
/>
)

await screen.findByTestId('cancel-button')
const fullscreenEditor = screen.getByTestId('value-full-screen-editor')
await userEvent.click(within(fullscreenEditor).getByRole('button', { name: /^cancel$/i }))

expect(onSave).not.toHaveBeenCalled()
expect(onOpenChange).toHaveBeenCalledWith(false)
})

it('should render the variable code editor when environment context is provided', async () => {
renderWithProviders(
<VariableValueEditorModal
open={true}
onOpenChange={jest.fn()}
onSave={jest.fn()}
value="initial value"
title="Value editor"
description="Edit the value in a larger editor."
language="yaml"
environmentId="environment-id"
serviceId="service-id"
scope="APPLICATION"
/>
)

await screen.findByTestId('submit-button')
expect(screen.getByTestId('mock-code-editor-variable')).toBeInTheDocument()
})

it('should render the plain code editor when no environment context is provided', async () => {
renderWithProviders(
<VariableValueEditorModal
open={true}
onOpenChange={jest.fn()}
onSave={jest.fn()}
value="initial value"
title="Value editor"
description="Edit the value in a larger editor."
language="plaintext"
/>
)

await screen.findByTestId('submit-button')
expect(screen.getByTestId('mock-code-editor')).toBeInTheDocument()
})
})
Loading
Loading