feat(frontend): improve table action UX (#4056)
* feat(frontend): improve table action UX * feat(frontend): improve table action UX
This commit is contained in:
@@ -1004,3 +1004,25 @@ export function maxHeight(
|
||||
|
||||
return Math.max(maxRowPerGrid, maxRows)
|
||||
}
|
||||
|
||||
export function isTableAction(id: string, app: App): boolean {
|
||||
const [tableId, actionId] = id.split('_')
|
||||
|
||||
if (!tableId || !actionId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const table = findGridItem(app, tableId)
|
||||
if (
|
||||
!table ||
|
||||
(table.data.type !== 'tablecomponent' &&
|
||||
table.data.type !== 'aggridcomponent' &&
|
||||
table.data.type !== 'aggridcomponentee' &&
|
||||
table.data.type !== 'dbexplorercomponent' &&
|
||||
table.data.type !== 'aggridinfinitecomponent' &&
|
||||
table.data.type !== 'aggridinfinitecomponentee')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
import { ccomponents, components } from '../component'
|
||||
import CssProperty from '../componentsPanel/CssProperty.svelte'
|
||||
import GridTab from './GridTab.svelte'
|
||||
import { deleteGridItem } from '../appUtils'
|
||||
import { deleteGridItem, isTableAction } from '../appUtils'
|
||||
import GridPane from './GridPane.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { push } from '$lib/history'
|
||||
import StylePanel from './StylePanel.svelte'
|
||||
import { ChevronLeft, ArrowBigUp } from 'lucide-svelte'
|
||||
import { ChevronLeft, ArrowBigUp, ArrowLeft } from 'lucide-svelte'
|
||||
import GridCondition from './GridCondition.svelte'
|
||||
import { isTriggerable } from './script/utils'
|
||||
import { inferDeps } from '../appUtilsInfer'
|
||||
@@ -42,6 +42,9 @@
|
||||
import ContextVariables from './ContextVariables.svelte'
|
||||
import EventHandlers from './EventHandlers.svelte'
|
||||
import GridNavbar from './GridNavbar.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
||||
undefined
|
||||
@@ -175,6 +178,47 @@
|
||||
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
{#if componentSettings?.item?.id && isTableAction(componentSettings?.item?.id, $app)}
|
||||
<div
|
||||
class="flex items-center px-3 py-2 bg-surface border-b text-xs font-semibold gap-2 justify-between"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">
|
||||
<div class="flex flex-row gap-1"> Back to table component </div>
|
||||
</svelte:fragment>
|
||||
<Button
|
||||
iconOnly
|
||||
startIcon={{
|
||||
icon: ArrowLeft
|
||||
}}
|
||||
size="xs"
|
||||
btnClasses={twMerge(
|
||||
'p-1 text-gray-300 hover:!text-gray-600 dark:text-gray-500 dark:hover:!text-gray-200 bg-transparent'
|
||||
)}
|
||||
on:click={() => {
|
||||
const tableId = componentSettings?.item?.id?.split?.('_')?.[0]
|
||||
|
||||
if (tableId) {
|
||||
$selectedComponent = [tableId]
|
||||
}
|
||||
}}
|
||||
color="light"
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
Table action of table
|
||||
<Badge color="indigo">{componentSettings?.item?.id.split('_')[0]}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocLink
|
||||
docLink="https://www.windmill.dev/docs/apps/app_configuration_settings/aggrid_table#table-actions"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if componentSettings?.item?.data}
|
||||
{@const component = componentSettings.item.data}
|
||||
<div class="flex justify-between items-center px-3 py-1 bg-surface-secondary">
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
import { Badge } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { getNextId } from '$lib/components/flows/idUtils'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { classNames, generateRandomString } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppViewerContext, BaseAppComponent } from '../../types'
|
||||
import { appComponentFromType } from '../appUtils'
|
||||
import type { ButtonComponent, CheckboxComponent, SelectComponent } from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import { Inspect, List, ToggleRightIcon, Trash } from 'lucide-svelte'
|
||||
import { GripVertical, Inspect, List, ToggleRightIcon } from 'lucide-svelte'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import CloseButton from '$lib/components/common/CloseButton.svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
export let components:
|
||||
| (BaseAppComponent & (ButtonComponent | CheckboxComponent | SelectComponent))[]
|
||||
@@ -21,9 +24,16 @@
|
||||
}
|
||||
})
|
||||
|
||||
let items =
|
||||
components?.map((tab, index) => {
|
||||
return { value: tab, id: generateRandomString(), originalIndex: index }
|
||||
}) ?? []
|
||||
|
||||
$: components = items.map((item) => item.value)
|
||||
|
||||
export let id: string
|
||||
|
||||
const { selectedComponent, app, errorByComponent } =
|
||||
const { selectedComponent, app, errorByComponent, hoverStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function addComponent(typ: 'buttoncomponent' | 'checkboxcomponent' | 'selectcomponent') {
|
||||
@@ -37,11 +47,21 @@
|
||||
...appComponentFromType(typ)(`${id}_${actionId}`),
|
||||
recomputeIds: []
|
||||
}
|
||||
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
value: newComponent,
|
||||
id: generateRandomString(),
|
||||
originalIndex: items.length
|
||||
}
|
||||
]
|
||||
|
||||
components = [...components, newComponent]
|
||||
$app = $app
|
||||
}
|
||||
|
||||
function deleteComponent(cid: string) {
|
||||
function deleteComponent(cid: string, index: number) {
|
||||
if (!components) {
|
||||
return
|
||||
}
|
||||
@@ -51,6 +71,20 @@
|
||||
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
|
||||
// Remove the corresponding item from the items array
|
||||
items = items.filter((item) => item.originalIndex !== index)
|
||||
}
|
||||
|
||||
function handleConsider(e: CustomEvent): void {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
}
|
||||
function handleFinalize(e: CustomEvent): void {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -59,43 +93,61 @@
|
||||
{#if components.length == 0}
|
||||
<span class="text-xs text-tertiary">No action buttons</span>
|
||||
{/if}
|
||||
{#each components as component}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold gap-1 truncate py-1.5 px-2 cursor-pointer transition-all justify-between flex items-center border border-gray-3 rounded-md',
|
||||
'bg-surface hover:bg-surface-hover focus:border-primary text-secondary',
|
||||
$selectedComponent?.includes(component.id) ? 'outline outline-blue-500 bg-red-400' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
$selectedComponent = [component.id]
|
||||
<div class="w-full flex gap-2 flex-col mt-2">
|
||||
<section
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
on:keypress
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
>
|
||||
<Badge color="dark-indigo">
|
||||
{component.id}
|
||||
</Badge>
|
||||
{#each items as item, index (item.id)}
|
||||
{@const component = items[index].value}
|
||||
|
||||
<div>
|
||||
{#if component.type == 'buttoncomponent'}
|
||||
Button
|
||||
{:else if component.type == 'selectcomponent'}
|
||||
Select
|
||||
{:else if component.type == 'checkboxcomponent'}
|
||||
Toggle
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
on:click={() => deleteComponent(component.id)}
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div animate:flip={{ duration: 200 }} class="flex flex-row gap-2 items-center mb-2">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs text-semibold truncate py-1.5 px-2 cursor-pointer justify-between flex items-center border rounded-md',
|
||||
'bg-surface hover:bg-surface-hover focus:border-primary text-secondary'
|
||||
)}
|
||||
on:click={() => {
|
||||
$selectedComponent = [component.id]
|
||||
}}
|
||||
on:mouseover={() => {
|
||||
$hoverStore = component.id
|
||||
}}
|
||||
on:keypress
|
||||
>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Badge color="dark-indigo">
|
||||
{component.id}
|
||||
</Badge>
|
||||
|
||||
<div>
|
||||
{#if component.type == 'buttoncomponent'}
|
||||
Button
|
||||
{:else if component.type == 'selectcomponent'}
|
||||
Select
|
||||
{:else if component.type == 'checkboxcomponent'}
|
||||
Toggle
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<CloseButton small on:close={() => deleteComponent(component.id, index)} />
|
||||
</div>
|
||||
</div>
|
||||
<div use:dragHandle class="handle w-4 h-4" aria-label="drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</div>
|
||||
<div class="w-full flex gap-2">
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-tertiary"
|
||||
|
||||
Reference in New Issue
Block a user