mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 08:00:59 +00:00
feat(frontend): Add ID renaming popup (#1344)
* feat(frontend): Add id renaming popup * fix(frontend): State reset * actually do it --------- Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
@@ -99,19 +99,17 @@
|
||||
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
|
||||
as JSON <input class="windmillapp" type="checkbox" bind:checked={forceJson} /></div
|
||||
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
|
||||
class="mb-2 min-w-[360px] text-sm text-gray-700 relative"
|
||||
class="mb-2 min-w-[300px] text-sm text-gray-700 relative"
|
||||
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
|
||||
<div class="text-gray-500 text-sm absolute top-0 right-2">
|
||||
<div class="text-gray-500 text-sm absolute top-0 right-2">
|
||||
<button on:click={jsonViewer.openDrawer}>Expand JSON</button>
|
||||
</div></div
|
||||
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md "
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md"
|
||||
>
|
||||
{#each Object.keys(result) as col}
|
||||
<div class="flex flex-col max-h-40 min-w-full">
|
||||
<div
|
||||
class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md "
|
||||
>
|
||||
<div class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md">
|
||||
{col}
|
||||
</div>
|
||||
{#if Array.isArray(result[col])}
|
||||
@@ -125,7 +123,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !forceJson && resultKind == 'table-row'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 "
|
||||
class="grid grid-flow-col-dense border border-gray-200"
|
||||
>
|
||||
<TableCustom>
|
||||
<tbody slot="body">
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
| 'textareainputcomponent' = 'textinputcomponent'
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, selectedComponent, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let placeholder: string | undefined = undefined
|
||||
let defaultValue: string | undefined = undefined
|
||||
@@ -50,7 +51,8 @@
|
||||
css?.input?.class ?? ''
|
||||
)}
|
||||
style="resize:none; {css?.input?.style ?? ''}"
|
||||
on:pointerdown|stopPropagation={(e) => selectId(e, id, selectedComponent, $app)}
|
||||
on:pointerdown|stopPropagation={(e) =>
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
bind:value
|
||||
{placeholder}
|
||||
/>
|
||||
@@ -63,7 +65,8 @@
|
||||
css?.input?.class ?? ''
|
||||
)}
|
||||
style={css?.input?.style ?? ''}
|
||||
on:pointerdown|stopPropagation={(e) => selectId(e, id, selectedComponent, $app)}
|
||||
on:pointerdown|stopPropagation={(e) =>
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
type="password"
|
||||
bind:value
|
||||
{placeholder}
|
||||
@@ -75,7 +78,8 @@
|
||||
css?.input?.class ?? ''
|
||||
)}
|
||||
style={css?.input?.style ?? ''}
|
||||
on:pointerdown|stopPropagation={(e) => selectId(e, id, selectedComponent, $app)}
|
||||
on:pointerdown|stopPropagation={(e) =>
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
type="text"
|
||||
bind:value
|
||||
{placeholder}
|
||||
@@ -87,7 +91,8 @@
|
||||
css?.input?.class ?? ''
|
||||
)}
|
||||
style={css?.input?.style ?? ''}
|
||||
on:pointerdown|stopPropagation={(e) => selectId(e, id, selectedComponent, $app)}
|
||||
on:pointerdown|stopPropagation={(e) =>
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
type="email"
|
||||
bind:value
|
||||
{placeholder}
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
on:focus={(e) => {
|
||||
if ($connectingInput.opened) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
on:pointerdown={onPointerDown}
|
||||
on:click|capture={(event) => preventInteraction(event, type === 'tabscomponent')}
|
||||
on:drag|capture={preventInteraction}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { allItems } from '$lib/components/apps/utils'
|
||||
import { forbiddenIds } from '$lib/components/flows/idUtils'
|
||||
import { ArrowRight, Pencil } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { fade, slide } from 'svelte/transition'
|
||||
import { Button, Popup } from '../../../../common'
|
||||
|
||||
const { app, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let id: string
|
||||
const dispatch = createEventDispatcher()
|
||||
const regex = /^[a-zA-Z0-9]{1,}$/
|
||||
let value = id
|
||||
let button: HTMLButtonElement
|
||||
let input: HTMLInputElement
|
||||
let error = ''
|
||||
|
||||
$: if (!regex.test(value)) {
|
||||
error = 'The ID must include only letters and numbers'
|
||||
} else if (forbiddenIds.includes(value)) {
|
||||
error = 'This ID is reserved'
|
||||
} else if (
|
||||
allItems($app.grid, $app.subgrids).some((item) => item.id === value && item.id !== id)
|
||||
) {
|
||||
error = 'This ID is already in use'
|
||||
} else {
|
||||
id = value
|
||||
error = ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (error != '') {
|
||||
return
|
||||
}
|
||||
dispatch('change', id)
|
||||
input.blur()
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click|stopPropagation={() => {
|
||||
$selectedComponent = [id]
|
||||
}}
|
||||
bind:this={button}
|
||||
title="Edit ID"
|
||||
class="flex items-center px-1 rounded-sm bg-gray-100 hover:text-black text-gray-600"
|
||||
aria-label="Open component ID editor"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<Popup
|
||||
ref={button}
|
||||
options={{ placement: 'top-start' }}
|
||||
transition={fade}
|
||||
wrapperClasses="!z-[1002]"
|
||||
outerClasses="rounded shadow-xl bg-white border p-3"
|
||||
on:close={() => (value = id)}
|
||||
>
|
||||
<label class="block text-gray-900">
|
||||
<div class="pb-1 text-sm text-gray-600">Component ID</div>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value
|
||||
class="!w-auto grow"
|
||||
bind:this={input}
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keypress={({ key }) => {
|
||||
if (key === 'Enter') save()
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Save ID"
|
||||
disabled={error != ''}
|
||||
on:click={save}
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
{#if error != ''}
|
||||
<div
|
||||
transition:slide|local={{ duration: 100 }}
|
||||
class="w-full text-sm text-red-600 whitespace-pre-wrap pt-1"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
</Popup>
|
||||
+95
-20
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext, ContextPanelContext } from '$lib/components/apps/types'
|
||||
import { allItems } from '$lib/components/apps/utils'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { ChevronDown, ChevronUp, Pointer } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { allsubIds } from '../../appUtils'
|
||||
import { allsubIds, findGridItem } from '../../appUtils'
|
||||
import IdEditor from './IdEditor.svelte'
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
@@ -43,18 +45,82 @@
|
||||
blue: 'bg-blue-500 text-white',
|
||||
indigo: 'bg-indigo-500 text-white'
|
||||
}
|
||||
|
||||
function renameId(newId: string): void {
|
||||
{
|
||||
const item = findGridItem($app, id)
|
||||
if (item) {
|
||||
item.data.id = newId
|
||||
item.id = newId
|
||||
}
|
||||
const oldSubgrids = Object.keys($app.subgrids ?? {}).filter((subgrid) =>
|
||||
subgrid.startsWith(id + '-')
|
||||
)
|
||||
oldSubgrids.forEach((subgrid) => {
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subgrid.replace(id, newId)] = $app.subgrids[subgrid]
|
||||
delete $app.subgrids[subgrid]
|
||||
}
|
||||
})
|
||||
allItems($app.grid, $app.subgrids).forEach((item) => {
|
||||
if (item.data.componentInput?.type == 'connected') {
|
||||
if (item.data.componentInput.connection?.componentId === id) {
|
||||
item.data.componentInput.connection.componentId = newId
|
||||
}
|
||||
} else if (item.data.componentInput?.type == 'runnable') {
|
||||
if (
|
||||
item.data.componentInput?.runnable?.type === 'runnableByName' &&
|
||||
item.data.componentInput?.runnable?.inlineScript?.refreshOn
|
||||
?.map((x) => x.id)
|
||||
?.includes(id)
|
||||
) {
|
||||
item.data.componentInput.runnable.inlineScript.refreshOn =
|
||||
item.data.componentInput.runnable.inlineScript.refreshOn.map((x) => {
|
||||
if (x.id === id) {
|
||||
return {
|
||||
id: newId,
|
||||
key: x.key
|
||||
}
|
||||
}
|
||||
return x
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(item.data.configuration ?? {}).forEach((config) => {
|
||||
if (config.type === 'connected') {
|
||||
if (config.connection?.componentId === id) {
|
||||
config.connection.componentId = newId
|
||||
}
|
||||
} else if (config.type == 'oneOf') {
|
||||
Object.values(config.configuration ?? {}).forEach((choices) => {
|
||||
Object.values(choices).forEach((c) => {
|
||||
if (c.type === 'connected') {
|
||||
if (c.connection?.componentId === id) {
|
||||
c.connection.componentId = newId
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
$app = $app
|
||||
$selectedComponent = [newId]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class={$search == '' || inSearch ? '' : 'invisible h-0 overflow-hidden'}>
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
on:mouseenter|stopPropagation={() => {
|
||||
if (id !== $hoverStore) {
|
||||
$hoverStore = id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
on:mouseleave|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
@@ -74,26 +140,35 @@
|
||||
$manuallyOpened[id] = $manuallyOpened[id] != undefined ? !$manuallyOpened[id] : true
|
||||
}}
|
||||
>
|
||||
<button
|
||||
disabled={!(selectable && !$selectedComponent?.includes(id)) || $connectingInput?.opened}
|
||||
title="Select component"
|
||||
on:click|stopPropagation={() => ($selectedComponent = [id])}
|
||||
class="flex items-center ml-0.5 rounded-sm bg-gray-100 hover:text-black text-gray-600"
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-2xs font-bold px-2 py-0.5 rounded-sm',
|
||||
$selectedComponent?.includes(id) ? idClass[color] : ''
|
||||
)}
|
||||
<div class="flex">
|
||||
<button
|
||||
disabled={!(selectable && !$selectedComponent?.includes(id)) || $connectingInput?.opened}
|
||||
title="Select component"
|
||||
on:click|stopPropagation={() => ($selectedComponent = [id])}
|
||||
class="flex items-center ml-0.5 rounded-sm bg-gray-100 hover:text-black text-gray-600"
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
{#if selectable && !$selectedComponent?.includes(id)}
|
||||
<div class=" px-1 ">
|
||||
<Pointer size={14} />
|
||||
<div
|
||||
class={classNames(
|
||||
'text-2xs font-bold px-2 py-0.5 rounded-sm',
|
||||
$selectedComponent?.includes(id) ? idClass[color] : ''
|
||||
)}
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
{#if selectable && !$selectedComponent?.includes(id)}
|
||||
<div class="px-1">
|
||||
<Pointer size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if selectable && ($selectedComponent?.includes(id) || $hoverStore === id)}
|
||||
<IdEditor
|
||||
{id}
|
||||
on:selected={() => ($selectedComponent = [id])}
|
||||
on:change={({ detail }) => renameId(detail)}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-2xs font-bold flex flex-row gap-2 items-center truncate">
|
||||
{name}
|
||||
{#if !open}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<svelte:options accessors />
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { slide, type TransitionConfig } from 'svelte/transition'
|
||||
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
|
||||
import { clickOutside } from '../../../utils'
|
||||
@@ -15,6 +15,7 @@
|
||||
export let closeOn: (keyof HTMLElementEventMap)[] = ['blur']
|
||||
export let innerClasses = ''
|
||||
export let outerClasses = ''
|
||||
export let wrapperClasses = ''
|
||||
export let transition: (node: Element, params?: Record<string, any>) => TransitionConfig = slide
|
||||
export { openFocusIn as open, closed as close }
|
||||
|
||||
@@ -28,6 +29,7 @@
|
||||
})
|
||||
|
||||
const [popperRef, popperContent, getInstance] = createPopperActions()
|
||||
const dispatch = createEventDispatcher()
|
||||
let popup: HTMLElement | undefined
|
||||
let focusableElements: HTMLElement[]
|
||||
|
||||
@@ -59,9 +61,11 @@
|
||||
if ($stateMachine.currentState === 'open-focus-out') {
|
||||
setTimeout(() => {
|
||||
stateMachine.setState('closed')
|
||||
dispatch('close')
|
||||
}, 0)
|
||||
} else {
|
||||
stateMachine.setState('closed')
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
function conditionalClosed() {
|
||||
@@ -148,7 +152,7 @@
|
||||
<svelte:window on:keydown={keyDown} />
|
||||
|
||||
<div
|
||||
class="z-50"
|
||||
class="z-50 {wrapperClasses}"
|
||||
bind:this={popup}
|
||||
use:popperContent={options}
|
||||
use:clickOutside
|
||||
@@ -157,7 +161,7 @@
|
||||
aria-expanded={$stateMachine.currentState !== 'closed'}
|
||||
>
|
||||
{#if $stateMachine.currentState !== 'closed'}
|
||||
<div transition:transition|local={{ duration: 200 }} class={outerClasses}>
|
||||
<div transition:transition|local={{ duration: 100 }} class={outerClasses}>
|
||||
<div class={innerClasses}>
|
||||
<slot open={openFocusIn} close={closed} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const aCharCode = 'a'.charCodeAt(0)
|
||||
|
||||
const forbiddenIds: string[] = ['do']
|
||||
export const forbiddenIds: string[] = ['do', 'bg', 'ctx', 'state']
|
||||
|
||||
export function numberToChars(n: number) {
|
||||
if (n < 0) {
|
||||
|
||||
Reference in New Issue
Block a user