mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
App input (#1353)
* feat(frontend): record frontend errors * feat(frontend): display result * feat(frontend): Fix name shadowing * feat(frontend): fix typo * fix(frontend): better display frontend errors * fix(frontend): wip * wip * feat(frontend): text input * feat(frontend): enable double click to open + disabled mode when input is computed or connected * feat(frontend): fix monaco setCode * feat(frontend): revert package.json changes * feat(frontend): fix syncing issues * feat(frontend): adapt style * feat(frontend): fix event propagation
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
import libStdContent from '$lib/es5.d.ts.txt?raw'
|
||||
import { editor as meditor, languages, Range, Uri as mUri } from 'monaco-editor'
|
||||
import { buildWorkerDefinition } from 'monaco-editor-workers'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
@@ -372,6 +373,17 @@
|
||||
let editor: meditor.IStandaloneCodeEditor
|
||||
let model: meditor.ITextModel
|
||||
|
||||
const { componentControl, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
if ($selectedComponent) {
|
||||
$componentControl[$selectedComponent[0]] = {
|
||||
setCode: (value: string) => {
|
||||
code = value
|
||||
setCode(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let code: string = ''
|
||||
export let hash: string = createHash()
|
||||
export let automaticLayout = true
|
||||
|
||||
@@ -29,13 +29,15 @@
|
||||
export let initializing: boolean | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'textcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let editorMode: boolean = false
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['textcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
const { app, worldStore, mode } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, mode, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorcontext = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
@@ -79,10 +81,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getClassesByType() {
|
||||
switch (resolvedConfig.style) {
|
||||
case 'Title':
|
||||
return 'h1-textarea'
|
||||
case 'Subtitle':
|
||||
return 'h3-textarea'
|
||||
case 'Body':
|
||||
return 'p-textarea'
|
||||
case 'Caption':
|
||||
return 'p-textarea'
|
||||
case 'Label':
|
||||
return 'label'
|
||||
default:
|
||||
return 'p-textarea'
|
||||
}
|
||||
}
|
||||
|
||||
let component = 'p'
|
||||
let classes = ''
|
||||
|
||||
function getHorizontalAlignement() {
|
||||
if (horizontalAlignment) {
|
||||
switch (horizontalAlignment) {
|
||||
case 'left':
|
||||
return '!text-left'
|
||||
case 'center':
|
||||
return '!text-center'
|
||||
case 'right':
|
||||
return '!text-right'
|
||||
default:
|
||||
return '!text-left'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: resolvedConfig.style && (component = getComponent())
|
||||
$: resolvedConfig.style && (classes = getClasses())
|
||||
$: initialValue = componentInput?.type == 'template' ? componentInput.eval : ''
|
||||
$: editableValue = initialValue ? JSON.parse(JSON.stringify(initialValue)) : ''
|
||||
|
||||
let rows = 1
|
||||
|
||||
function onInput(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
|
||||
if (target.value) {
|
||||
$componentControl[id]?.setCode?.(target.value)
|
||||
editableValue
|
||||
autosize()
|
||||
}
|
||||
}
|
||||
|
||||
function autosize() {
|
||||
const el = document.getElementById(`text-${id}`)
|
||||
setTimeout(() => {
|
||||
if (el !== null) {
|
||||
el.style.cssText = 'height:auto; padding:0'
|
||||
el.style.cssText = 'height:' + el.scrollHeight + 'px'
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['textcomponent'].initialData.configuration) as key (key)}
|
||||
@@ -95,58 +154,99 @@
|
||||
{/each}
|
||||
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
<div class="h-full w-full overflow-hidden">
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
{#if !result || result === ''}
|
||||
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
|
||||
No text
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex flex-wrap gap-2 pb-0.5 {$mode === 'dnd' && componentInput?.type == 'template'
|
||||
? 'cursor-text'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if ($mode === 'dnd' && componentInput?.type == 'template') {
|
||||
let ontextfocus = editorcontext?.ontextfocus
|
||||
if (ontextfocus) {
|
||||
get(ontextfocus)?.()
|
||||
}
|
||||
<div
|
||||
class="h-full w-full overflow-hidden"
|
||||
on:dblclick={() => {
|
||||
if (!editorMode) {
|
||||
editorMode = true
|
||||
document.getElementById(`text-${id}`)?.focus()
|
||||
autosize()
|
||||
}
|
||||
}}
|
||||
on:keydown|stopPropagation
|
||||
>
|
||||
{#if editorMode && componentInput?.type == 'template'}
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<textarea
|
||||
class={twMerge(
|
||||
'whitespace-pre-wrap !outline-none !border-0 !bg-transparent !resize-none !overflow-hidden !ring-0 !p-0 text-center',
|
||||
$app.css?.['textcomponent']?.['text']?.class,
|
||||
customCss?.text?.class,
|
||||
classes,
|
||||
getClasses(),
|
||||
getClassesByType(),
|
||||
getHorizontalAlignement()
|
||||
)}
|
||||
on:pointerdown|stopPropagation
|
||||
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
|
||||
id={`text-${id}`}
|
||||
on:pointerenter={() => {
|
||||
const elem = document.getElementById(`text-${id}`)
|
||||
if (elem) {
|
||||
elem.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:element
|
||||
this={component}
|
||||
class={twMerge(
|
||||
'whitespace-pre-wrap',
|
||||
$app.css?.['textcomponent']?.['text']?.class,
|
||||
customCss?.text?.class,
|
||||
classes
|
||||
)}
|
||||
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
|
||||
{rows}
|
||||
on:input={onInput}
|
||||
value={editableValue}
|
||||
/>
|
||||
</AlignWrapper>
|
||||
{:else}
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
{#if !result || result === ''}
|
||||
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
|
||||
No text
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex flex-wrap gap-2 pb-0.5 {$mode === 'dnd' &&
|
||||
componentInput?.type == 'template'
|
||||
? 'cursor-text'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if ($mode === 'dnd' && componentInput?.type == 'template') {
|
||||
let ontextfocus = editorcontext?.ontextfocus
|
||||
if (ontextfocus) {
|
||||
get(ontextfocus)?.()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{String(result)}
|
||||
{#if resolvedConfig.tooltip != ''}
|
||||
<Tooltip>{resolvedConfig.tooltip}</Tooltip>
|
||||
{/if}
|
||||
{#if resolvedConfig.copyButton && result}
|
||||
<Popover notClickable>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
color="dark"
|
||||
btnClasses="!p-1"
|
||||
on:click={() => copyToClipboard(result)}
|
||||
>
|
||||
<Clipboard size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
<svelte:fragment slot="text">Copy to clipboard</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
</div>
|
||||
{/if}
|
||||
</AlignWrapper>
|
||||
<svelte:element
|
||||
this={component}
|
||||
class={twMerge(
|
||||
'whitespace-pre-wrap',
|
||||
$app.css?.['textcomponent']?.['text']?.class,
|
||||
customCss?.text?.class,
|
||||
classes
|
||||
)}
|
||||
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(
|
||||
';'
|
||||
)}
|
||||
>
|
||||
{String(result)}
|
||||
{#if resolvedConfig.tooltip != ''}
|
||||
<Tooltip>{resolvedConfig.tooltip}</Tooltip>
|
||||
{/if}
|
||||
{#if resolvedConfig.copyButton && result}
|
||||
<Popover notClickable>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
color="dark"
|
||||
btnClasses="!p-1"
|
||||
on:click={() => copyToClipboard(result)}
|
||||
>
|
||||
<Clipboard size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
<svelte:fragment slot="text">Copy to clipboard</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
</div>
|
||||
{/if}
|
||||
</AlignWrapper>
|
||||
{/if}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { HorizontalAlignment, VerticalAlignment } from '../../types'
|
||||
import { tailwindHorizontalAlignment, tailwindVerticalAlignment } from '../../utils'
|
||||
|
||||
export let horizontalAlignment: HorizontalAlignment | undefined = undefined
|
||||
export let verticalAlignment: VerticalAlignment | undefined = undefined
|
||||
@@ -10,26 +11,6 @@
|
||||
export let style = ''
|
||||
export let render: boolean = true
|
||||
|
||||
function tailwindHorizontalAlignment(alignment?: HorizontalAlignment) {
|
||||
if (!alignment) return ''
|
||||
const classes: Record<HorizontalAlignment, string> = {
|
||||
left: 'justify-start',
|
||||
center: 'justify-center',
|
||||
right: 'justify-end'
|
||||
}
|
||||
return classes[alignment]
|
||||
}
|
||||
|
||||
function tailwindVerticalAlignment(alignment?: VerticalAlignment) {
|
||||
if (!alignment) return ''
|
||||
const classes: Record<VerticalAlignment, string> = {
|
||||
top: 'items-start',
|
||||
center: 'items-center',
|
||||
bottom: 'items-end'
|
||||
}
|
||||
return classes[alignment]
|
||||
}
|
||||
|
||||
$: classes = twMerge(
|
||||
'flex z-auto',
|
||||
noWFull ? '' : 'w-full',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { Anchor, Bug, Expand, Move } from 'lucide-svelte'
|
||||
import { Anchor, Bug, Expand, Move, Pen } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { Alert, Button } from '$lib/components/common'
|
||||
@@ -14,6 +14,8 @@
|
||||
export let locked: boolean = false
|
||||
export let hover: boolean = false
|
||||
export let shouldHideActions: boolean = false
|
||||
export let hasInlineEditor: boolean = false
|
||||
export let inlineEditorOpened: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -49,6 +51,23 @@
|
||||
|
||||
{#if selected && !shouldHideActions}
|
||||
<div class="top-[-9px] -right-[8px] flex flex-row absolute gap-1.5 z-50">
|
||||
{#if hasInlineEditor}
|
||||
<button
|
||||
title="Expand"
|
||||
class={classNames(
|
||||
'px-1 text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
|
||||
'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if inlineEditorOpened}
|
||||
<Pen aria-label="Unlock position" size={14} class="text-orange-500" />
|
||||
{:else}
|
||||
<Pen aria-label="Lock position" size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
title="Expand"
|
||||
class={classNames(
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
|
||||
let initializing: boolean | undefined = undefined
|
||||
let componentContainerHeight: number = 0
|
||||
|
||||
let inlineEditorOpened: boolean = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
@@ -81,6 +83,13 @@
|
||||
on:lock
|
||||
on:expand
|
||||
{locked}
|
||||
{inlineEditorOpened}
|
||||
hasInlineEditor={component.type === 'textcomponent' &&
|
||||
component.componentInput &&
|
||||
component.componentInput.type !== 'connected'}
|
||||
on:triggerInlineEditor={() => {
|
||||
inlineEditorOpened = !inlineEditorOpened
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -207,6 +216,7 @@
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
bind:editorMode={inlineEditorOpened}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
noVariablePicker
|
||||
/>
|
||||
{:else if componentSettings.item.data.componentInput.type === 'template' && componentSettings.item.data.componentInput !== undefined}
|
||||
<div class="py-1 min-h-[28px] rounded border border-1 border-gray-500">
|
||||
<div class="py-1 min-h-[28px] rounded border border-1 border-gray-500">
|
||||
<TemplateEditor
|
||||
bind:this={editor}
|
||||
fontSize={12}
|
||||
|
||||
@@ -159,6 +159,7 @@ export type AppViewerContext = {
|
||||
right?: (skipTableActions?: boolean | undefined) => string | boolean
|
||||
setTab?: (index: number) => void
|
||||
agGrid?: { api: any; columnApi: any }
|
||||
setCode?: (value: string) => void
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
@@ -6,7 +6,13 @@ import { twMerge } from 'tailwind-merge'
|
||||
import type { AppComponent } from './editor/component'
|
||||
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
|
||||
import type { Output } from './rx'
|
||||
import type { App, ComponentCssProperty, GridItem } from './types'
|
||||
import type {
|
||||
App,
|
||||
ComponentCssProperty,
|
||||
GridItem,
|
||||
HorizontalAlignment,
|
||||
VerticalAlignment
|
||||
} from './types'
|
||||
|
||||
export function allItems(
|
||||
grid: GridItem[],
|
||||
@@ -262,3 +268,23 @@ export function concatCustomCss<T extends Record<string, ComponentCssProperty>>(
|
||||
})
|
||||
) as T
|
||||
}
|
||||
|
||||
export function tailwindHorizontalAlignment(alignment?: HorizontalAlignment) {
|
||||
if (!alignment) return ''
|
||||
const classes: Record<HorizontalAlignment, string> = {
|
||||
left: 'justify-start',
|
||||
center: 'justify-center',
|
||||
right: 'justify-end'
|
||||
}
|
||||
return classes[alignment]
|
||||
}
|
||||
|
||||
export function tailwindVerticalAlignment(alignment?: VerticalAlignment) {
|
||||
if (!alignment) return ''
|
||||
const classes: Record<VerticalAlignment, string> = {
|
||||
top: 'items-start',
|
||||
center: 'items-center',
|
||||
bottom: 'items-end'
|
||||
}
|
||||
return classes[alignment]
|
||||
}
|
||||
|
||||
@@ -482,6 +482,46 @@ const config = {
|
||||
fontFamily: theme('fontFamily.mono'),
|
||||
fontSize: theme('fontSize.sm') + ' !important',
|
||||
lineHeight: theme('lineHeight.4') + ' !important'
|
||||
},
|
||||
'.h1-textarea': {
|
||||
fontSize: '24px !important',
|
||||
fontWeight: `${theme('fontWeight.extrabold')} !important`,
|
||||
lineHeight: '1.05 !important',
|
||||
color: theme('!colors.gray.800'),
|
||||
[`@media (min-width: ${theme('screens.lg')})`]: {
|
||||
fontSize: '26px !important'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '29px !important'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '34px !important'
|
||||
}
|
||||
},
|
||||
'.h3-textarea': {
|
||||
fontSize: '18px !important',
|
||||
fontWeight: `${theme('fontWeight.bold')} !important`,
|
||||
lineHeight: '1.2 !important',
|
||||
color: theme('!colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '20px !important'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '22px !important'
|
||||
}
|
||||
},
|
||||
'.p-textarea': {
|
||||
fontSize: '16px !important',
|
||||
fontWeight: `${theme('fontWeight.normal')} !important`,
|
||||
lineHeight: '1.5 !important',
|
||||
color: theme('!colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '18px !important'
|
||||
},
|
||||
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '20px !important'
|
||||
}
|
||||
}
|
||||
})
|
||||
addComponents({
|
||||
|
||||
Reference in New Issue
Block a user