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:
Ádám Kovács
2023-04-02 15:46:24 +02:00
committed by GitHub
parent 3f1fd31e1b
commit a69feda6bf
7 changed files with 218 additions and 36 deletions
@@ -99,19 +99,17 @@
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
as JSON&nbsp;<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>
@@ -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 -1
View File
@@ -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) {