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 @@ -231,7 +231,7 @@ export function Versions({
className={cn(
'flex h-[36px] cursor-pointer items-center px-4 transition-colors duration-100',
isSelected
? 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover-hover:bg-[color-mix(in_srgb,var(--accent)_15%,transparent)]'
? 'bg-[color-mix(in_srgb,hsl(var(--accent))_10%,transparent)] hover-hover:bg-[color-mix(in_srgb,hsl(var(--accent))_15%,transparent)]'
: 'hover-hover:bg-[var(--surface-6)] dark:hover-hover:bg-[var(--border)]'
)}
onClick={() => handleRowClick(v.version)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,7 @@ export function ConditionInput({
overlayRefs.current.delete(block.id)
}
}}
data-preview-full-opacity={isPreview || undefined}
className={cn(
'pointer-events-none absolute inset-0 box-border overflow-auto whitespace-pre-wrap break-words border border-transparent bg-transparent px-3 py-2 font-sans text-sm',
(isPreview || disabled) && 'opacity-50'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export function EvalInput({
className='text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50'
/>
<div
data-preview-full-opacity={isPreview || undefined}
className={cn(
'pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm',
(isPreview || disabled) && 'opacity-50'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,13 @@ function SingleFileSelector({
iconSize='compact'
className='-translate-y-1/2 absolute top-1/2 right-[28px] z-10'
onClick={onClear}
disabled={isDeleting}
disabled={disabled || isDeleting}
data-preview-full-opacity={disabled || undefined}
>
{isDeleting ? (
<div className='size-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<X className='size-4 opacity-50 hover-hover:opacity-100' />
<X className='size-4 opacity-50 hover-hover:opacity-100' data-preview-full-opacity />
Comment thread
greptile-apps[bot] marked this conversation as resolved.
)}
</Button>
</div>
Expand Down Expand Up @@ -770,12 +771,13 @@ export function FileUpload({
iconSize='compact'
className='-translate-y-1/2 absolute top-1/2 right-[4px]'
onClick={(e) => handleRemoveFile(file, e)}
disabled={isDeleting}
disabled={disabled || isPreview || isDeleting}
data-preview-full-opacity={isPreview || undefined}
>
{isDeleting ? (
<div className='size-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<X className='size-4 opacity-50' />
<X className='size-4 opacity-50' data-preview-full-opacity />
)}
</Button>
</div>
Expand Down Expand Up @@ -972,7 +974,7 @@ export function FileUpload({
onOpenChange={(open) => {
if (open) void refetchWorkspaceFiles()
}}
disabled={disabled}
disabled={disabled || isPreview}
isLoading={loadingWorkspaceFiles}
formatFileSize={formatFileSize}
truncateMiddle={truncateMiddle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function GroupedCheckboxList({
)}
>
<span className='flex flex-1 items-center gap-2 truncate text-[var(--text-muted)]'>
<ManageWorkspace className='size-4 shrink-0 opacity-50' />
<ManageWorkspace className='size-4 shrink-0 opacity-50' data-preview-full-opacity />
<span className='truncate'>Configure PII Types</span>
</span>
<SelectedCountDisplay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export function LongInput({
/>
<div
ref={overlayRef}
data-preview-full-opacity={isPreview || undefined}
className={cn(
'absolute inset-0 box-border overflow-auto whitespace-pre-wrap break-words border border-transparent bg-transparent px-2 py-2 font-sans text-sm',
(isPreview || disabled) && 'opacity-50',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ export function MessagesInput({
{currentMessages.map((message, index) => (
<div
key={messageIdsRef.current[index] ?? `fallback-${index}`}
data-preview-full-opacity={isPreview || undefined}
className={cn(
'relative flex w-full flex-col rounded-sm border border-[var(--border-1)] bg-[var(--surface-5)] transition-colors dark:bg-[var(--surface-5)]',
disabled && 'opacity-50'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ export const ShortInput = memo(function ShortInput({
/>
<div
ref={overlayRef}
data-preview-full-opacity={isPreview || undefined}
className={cn(
'absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 pr-3 font-sans text-foreground text-sm [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
(isPreview || disabled) && 'opacity-50',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function SliderInput({
const isDisabled = isPreview || disabled

return (
<div className={cn('relative pt-2 pb-[22px]', isDisabled && 'opacity-50')}>
<div
data-preview-full-opacity={isPreview || undefined}
className={cn('relative pt-2 pb-[22px]', isDisabled && 'opacity-50')}
>
<Slider
value={[normalizedValue]}
min={min}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ export function FieldFormat({
ref={(el) => {
if (el) descriptionOverlayRefs.current[field.id] = el
}}
data-preview-full-opacity={isPreview || undefined}
style={{ scrollbarWidth: 'none' }}
className={cn(
'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,7 @@ export const ToolInput = memo(function ToolInput({
onDrop={(e) => handleDrop(e, toolIndex)}
>
<div
data-preview-full-opacity={isPreview || undefined}
className={cn(
'flex items-center justify-between gap-2 rounded-t-[4px] bg-[var(--surface-4)] px-2 py-[6.5px]',
(isCustomTool || hasToolBody) && 'cursor-pointer',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ function SubBlockComponent({
onMouseDown={handleMouseDown}
data-workflow-search-subblock-id={config.id}
data-workflow-search-canonical-id={config.canonicalParamId ?? config.id}
data-preview-readonly={(isPreview && !disabled) || undefined}
className='subblock-content flex flex-col gap-2.5'
>
{renderLabel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels'
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu'
import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles'
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow'
import { getBlock } from '@/blocks'
import { BlockTile } from '@/blocks/block-tile'
Expand All @@ -54,31 +55,6 @@ import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { useContextMenu } from '@/hooks/use-context-menu'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'

/**
* CSS override to show full opacity and prevent interaction in readonly preview mode.
* Extracted to avoid duplicating the style block in multiple places.
*/
const READONLY_PREVIEW_STYLES = `
.readonly-preview,
.readonly-preview * {
cursor: default !important;
}
.readonly-preview [disabled],
.readonly-preview [data-disabled],
.readonly-preview input,
.readonly-preview textarea,
.readonly-preview [role="combobox"],
.readonly-preview [role="slider"],
.readonly-preview [role="switch"],
.readonly-preview [role="checkbox"] {
opacity: 1 !important;
pointer-events: none;
}
.readonly-preview .opacity-50 {
opacity: 1 !important;
}
`

/**
* Format a value for display as JSON string
*/
Expand Down Expand Up @@ -606,7 +582,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro
return (
<div className='flex-1 overflow-y-auto overflow-x-hidden pt-2 pb-2'>
{/* Type Selection - matches SubflowEditor */}
<div>
<div data-preview-readonly>
<Label className='mb-[6.5px] block pl-0.5 text-[var(--text-primary)] text-small'>
{isLoop ? 'Loop Type' : 'Parallel Type'}
</Label>
Expand All @@ -629,7 +605,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro
</Label>

{isCountMode ? (
<div>
<div data-preview-readonly>
<Input
type='text'
value={iterations.toString()}
Expand Down Expand Up @@ -1431,7 +1407,6 @@ function PreviewEditorContent({
...subBlockValues,
__canonicalModes: canonicalModeOverrides,
}}
disabled={true}
/>
{index < visibleSubBlocks.length - 1 && (
<FieldDivider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @vitest-environment jsdom
*/
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('next/navigation', () => ({ useParams: () => ({ workspaceId: 'workspace-1' }) }))
vi.mock('@/hooks/use-webhook-management', () => ({
useWebhookManagement: () => ({ webhookUrl: null }),
}))
vi.mock(
'@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value',
() => ({ useSubBlockValue: () => [undefined, vi.fn()] })
)

import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles'
import type { SubBlockConfig } from '@/blocks/types'

const config: SubBlockConfig = { id: 'enabled', type: 'switch', title: 'Enabled' }

let container: HTMLDivElement
let root: Root

beforeEach(() => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})

afterEach(() => {
act(() => root.unmount())
container.remove()
})

describe('workflow preview read-only appearance', () => {
it('shows a preview value at full opacity while retaining the disabled interaction state', () => {
act(() =>
root.render(
<>
<style>{`button[disabled] { opacity: 0.5; } ${READONLY_PREVIEW_STYLES}`}</style>
<div className='readonly-preview'>
<div data-testid='preview'>
<SubBlock
blockId='block-1'
config={config}
isPreview
subBlockValues={{ enabled: { value: true } }}
/>
<div data-preview-readonly>
<button type='button' disabled>
Remove file
</button>
</div>
</div>
<div data-testid='disabled'>
<SubBlock blockId='block-1' config={config} disabled />
</div>
</div>
</>
)
)

const preview = container.querySelector('[data-testid="preview"]')!
const previewSwitch = preview.querySelector('[role="switch"]') as HTMLButtonElement
const disabled = container.querySelector('[data-testid="disabled"]')!
const disabledSwitch = disabled.querySelector('[role="switch"]') as HTMLButtonElement

expect(preview.querySelector('[data-preview-readonly]')).not.toBeNull()
expect(previewSwitch.hasAttribute('disabled')).toBe(true)
expect(previewSwitch.getAttribute('aria-checked')).toBe('true')
expect(getComputedStyle(previewSwitch).opacity).toBe('1')
expect(getComputedStyle(previewSwitch).pointerEvents).toBe('none')
const removeButton = preview.querySelector('button:not([role="switch"])') as HTMLButtonElement
expect(getComputedStyle(removeButton).pointerEvents).toBe('none')
expect(getComputedStyle(removeButton).opacity).toBe('0.5')

act(() => {
previewSwitch.click()
previewSwitch.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
})
expect(previewSwitch.getAttribute('aria-checked')).toBe('true')

expect(disabled.querySelector('[data-preview-readonly]')).toBeNull()
expect(disabledSwitch.hasAttribute('disabled')).toBe(true)
expect(getComputedStyle(disabledSwitch).opacity).toBe('0.5')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Keep preview fields noninteractive while showing intentionally read-only controls
* and their marked value overlays at full opacity in both preview sections.
*/
export const READONLY_PREVIEW_STYLES = `
.readonly-preview,
.readonly-preview * {
cursor: default !important;
}
.readonly-preview [data-preview-readonly] :is(
input,
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.
textarea,
[role="combobox"],
[role="slider"],
[role="switch"],
[role="checkbox"]
) {
opacity: 1 !important;
pointer-events: none;
}
.readonly-preview [data-preview-readonly] :is(button, [role="button"]) {
pointer-events: none;
}
.readonly-preview [data-preview-readonly] [data-preview-full-opacity] {
opacity: 1 !important;
}
`