feat(frontend): Copy, Cut and Paste (#1279)
* feat(frontend): add copy, paste and cut * feat(frontend): simplify code * feat(frontend): add apple modifiers --------- Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tab, Tabs } from '$lib/components/common'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
@@ -27,6 +28,12 @@
|
||||
let noPadding: boolean | undefined = undefined
|
||||
let tabHeight: number = 0
|
||||
|
||||
$: outputs = $worldStore
|
||||
? initOutput($worldStore, id, {
|
||||
selectedTabIndex: 0
|
||||
})
|
||||
: undefined
|
||||
|
||||
function handleTabSelection() {
|
||||
const selectedIndex = tabs?.indexOf(selected)
|
||||
outputs?.selectedTabIndex.set(selectedIndex, true)
|
||||
@@ -76,10 +83,6 @@
|
||||
|
||||
$: selected && handleTabSelection()
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
selectedTabIndex: Output<number>
|
||||
}
|
||||
|
||||
$: selectedIndex = tabs?.indexOf(selected) ?? -1
|
||||
|
||||
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
|
||||
@@ -88,7 +91,7 @@
|
||||
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
|
||||
|
||||
<div>
|
||||
<div bind:clientHeight={tabHeight} on:pointerdown|stopPropagation>
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
|
||||
{#each tabs ?? [] as res}
|
||||
<Tab
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
</div>
|
||||
</Tab>
|
||||
<div slot="content" class="h-full overflow-y-auto pb-4">
|
||||
<TabContent class="overflow-auto" value="settings">
|
||||
<TabContent class="overflow-auto h-full" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
{:else}
|
||||
|
||||
@@ -491,12 +491,12 @@
|
||||
<ToggleButton position="left" value="dnd" size="xs">
|
||||
<div class="inline-flex gap-1 items-center">
|
||||
<Pencil size={14} />
|
||||
<span class="hidden md:inline">Editor</span>
|
||||
<span class="hidden lg:inline">Editor</span>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="preview" size="xs">
|
||||
<div class="inline-flex gap-1 items-center">
|
||||
<Eye size={14} /> <span class="hidden md:inline">Preview</span>
|
||||
<Eye size={14} /> <span class="hidden lg:inline">Preview</span>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
@@ -530,19 +530,21 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if $focusedGrid !== undefined}
|
||||
<Badge color="indigo">
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</Badge>
|
||||
<div class="hidden lg:block">
|
||||
<Badge color="indigo">
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
@@ -592,7 +594,7 @@
|
||||
variant="border"
|
||||
startIcon={{ icon: faBug }}
|
||||
>
|
||||
<span class="hidden md:inline">Debug Runs</span>
|
||||
<span class="hidden xl:inline">Debug Runs</span>
|
||||
</Button>
|
||||
</span>
|
||||
<AppExportButton bind:this={appExport} />
|
||||
@@ -603,7 +605,7 @@
|
||||
variant="border"
|
||||
startIcon={{ icon: faExternalLink }}
|
||||
>
|
||||
<span class="hidden md:inline">Publish</span>
|
||||
<span class="hidden xl:inline">Publish</span>
|
||||
</Button>
|
||||
{#if appPath == ''}
|
||||
<Button
|
||||
|
||||
@@ -41,13 +41,14 @@
|
||||
|
||||
{#if pointerdown || selected || hover}
|
||||
<button
|
||||
title="Position locking"
|
||||
title="Lock Position"
|
||||
class={classNames(
|
||||
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[2.5rem] z-50 cursor-pointer',
|
||||
' hover:bg-gray-300',
|
||||
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
|
||||
)}
|
||||
on:click={() => dispatch('lock')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if locked}
|
||||
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
|
||||
@@ -63,6 +64,7 @@
|
||||
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
|
||||
)}
|
||||
on:click={() => dispatch('expand')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<Expand aria-label="Expand position" size={14} />
|
||||
</button>
|
||||
|
||||
@@ -218,8 +218,9 @@
|
||||
}}
|
||||
on:expand={() => {
|
||||
push(history, $app)
|
||||
$selectedComponent = gridComponent.data.id
|
||||
expandGriditem($app.grid, gridComponent, $breakpoint)
|
||||
$app = { ...$app }
|
||||
$app = $app
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -136,11 +136,11 @@
|
||||
if (!parentGridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
$selectedComponent = gridComponent.data.id
|
||||
push(history, $app)
|
||||
|
||||
expandGriditem(subGrid, gridComponent, $breakpoint, parentGridItem)
|
||||
$app = { ...$app }
|
||||
$app = $app
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getRecommendedDimensionsByComponent, type AppComponent } from './compon
|
||||
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
|
||||
import { gridColumns } from '../gridUtils'
|
||||
import { allItems } from '../utils'
|
||||
import type { Output, World } from '../rx'
|
||||
|
||||
function findGridItemById(
|
||||
root: GridItem[],
|
||||
@@ -18,7 +19,7 @@ function findGridItemById(
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function findGridItemParentId(app: App, id: string): string | undefined {
|
||||
export function findGridItemParentGrid(app: App, id: string): string | undefined {
|
||||
const gridItem = app.grid.find((x) => x.id === id)
|
||||
if (gridItem) {
|
||||
return undefined
|
||||
@@ -27,7 +28,7 @@ export function findGridItemParentId(app: App, id: string): string | undefined {
|
||||
const subGrid = app.subgrids[key]
|
||||
const gridItem = subGrid.find((x) => x.id === id)
|
||||
if (gridItem) {
|
||||
return key.split('-')[0]
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,9 +47,6 @@ export function getNextGridItemId(app: App): string {
|
||||
}
|
||||
|
||||
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
|
||||
const appComponent = data
|
||||
|
||||
appComponent.id = id
|
||||
|
||||
const newComponent = {
|
||||
resizable: true,
|
||||
@@ -57,7 +55,8 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
y: 0
|
||||
}
|
||||
|
||||
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
|
||||
let newData: AppComponent = JSON.parse(JSON.stringify(data))
|
||||
newData.id = id
|
||||
|
||||
const newItem: GridItem = {
|
||||
data: newData,
|
||||
@@ -65,7 +64,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
}
|
||||
|
||||
gridColumns.forEach((column) => {
|
||||
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
|
||||
const rec = getRecommendedDimensionsByComponent(newData.type, column)
|
||||
|
||||
newItem[column] = {
|
||||
...newComponent,
|
||||
@@ -84,6 +83,16 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
return newItem
|
||||
}
|
||||
|
||||
export function getGridItems(app: App, focusedGrid: FocusedGrid | undefined): GridItem[] {
|
||||
if (!focusedGrid) {
|
||||
return app.grid
|
||||
} else {
|
||||
const { parentComponentId, subGridIndex } = focusedGrid
|
||||
const key = `${parentComponentId}-${subGridIndex ?? 0}`
|
||||
return app?.subgrids?.[key] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
export function insertNewGridItem(
|
||||
app: App,
|
||||
data: AppComponent,
|
||||
@@ -96,18 +105,7 @@ export function insertNewGridItem(
|
||||
app.subgrids = {}
|
||||
}
|
||||
|
||||
if (!focusedGrid) {
|
||||
const newItem = createNewGridItem(app.grid, id, data)
|
||||
app.grid.push(newItem)
|
||||
} else {
|
||||
const { parentComponentId, subGridIndex } = focusedGrid
|
||||
|
||||
const key = `${parentComponentId}-${subGridIndex ?? 0}`
|
||||
|
||||
const subGrid = app.subgrids[key] ?? []
|
||||
subGrid.push(createNewGridItem(subGrid, id, data))
|
||||
app.subgrids[key] = subGrid
|
||||
}
|
||||
// We only want to set subgrids when we are not moving
|
||||
if (!keepId) {
|
||||
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
|
||||
@@ -115,6 +113,16 @@ export function insertNewGridItem(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const key = focusedGrid ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` : undefined
|
||||
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
||||
|
||||
const newItem = createNewGridItem(grid, id, data)
|
||||
grid.push(newItem)
|
||||
|
||||
if (focusedGrid) {
|
||||
app.subgrids[key!] = grid
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -169,27 +177,7 @@ export function deleteGridItem(
|
||||
return components
|
||||
}
|
||||
|
||||
export function duplicateGridItem(
|
||||
app: App,
|
||||
parent: string | undefined,
|
||||
id: string
|
||||
): string | undefined {
|
||||
const gridItem = findGridItem(app, id)
|
||||
|
||||
if (gridItem) {
|
||||
const newId = getNextGridItemId(app)
|
||||
const newItem = JSON.parse(JSON.stringify(gridItem))
|
||||
newItem.id = newId
|
||||
newItem.data.id = newId
|
||||
|
||||
let focusedGrid = parent
|
||||
? { parentComponentId: parent.split('-')[0], subGridIndex: Number(parent.split('-')[1]) }
|
||||
: undefined
|
||||
|
||||
return insertNewGridItem(app, newItem.data, focusedGrid)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
type AvailableSpace = {
|
||||
left: number
|
||||
@@ -206,7 +194,7 @@ export function findAvailableSpace(
|
||||
): AvailableSpace | undefined {
|
||||
if (gridItem) {
|
||||
const breakpoint = editorBreakpoint === 'sm' ? 3 : 12
|
||||
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 12
|
||||
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16
|
||||
const maxWidth = 12
|
||||
|
||||
const availableSpace = {
|
||||
@@ -297,6 +285,22 @@ function isOverlapping(item1: any, item2: any) {
|
||||
)
|
||||
}
|
||||
|
||||
type Outputtable<Type> = {
|
||||
-readonly [Property in keyof Type]: Output<Type[Property]>;
|
||||
};
|
||||
|
||||
|
||||
export function initOutput<I extends Record<string, any>>(world: World, id: string, init: I): Outputtable<I> {
|
||||
const output = world.outputsById[id] as Outputtable<I>
|
||||
if (init) {
|
||||
for (const key in init) {
|
||||
if (output && output[key] && output[key].peak() == undefined) {
|
||||
output[key].set(init[key] as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
export function expandGriditem(
|
||||
grid: GridItem[],
|
||||
gridComponent: GridItem,
|
||||
@@ -305,16 +309,17 @@ export function expandGriditem(
|
||||
) {
|
||||
const availableSpace = findAvailableSpace(grid, gridComponent, $breakpoint, parentGridItem)
|
||||
|
||||
console.log(availableSpace)
|
||||
if (!availableSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
const { left, right, top, bottom } = availableSpace
|
||||
const width = $breakpoint === 'sm' ? 3 : 12
|
||||
const previousGridItem = JSON.parse(JSON.stringify(gridComponent[width]))
|
||||
const item = gridComponent[width]
|
||||
|
||||
gridComponent[width].x = previousGridItem.x - left
|
||||
gridComponent[width].y = previousGridItem.y - top
|
||||
gridComponent[width].w = previousGridItem.w + left + right
|
||||
gridComponent[width].h = previousGridItem.h + top + bottom
|
||||
item.x = item.x - left
|
||||
item.y = item.y - top
|
||||
item.w = item.w + left + right
|
||||
item.h = item.h + top + bottom
|
||||
}
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import {
|
||||
deleteGridItem,
|
||||
findGridItem,
|
||||
findGridItemParentGrid,
|
||||
getAllSubgridsAndComponentIds,
|
||||
getGridItems,
|
||||
insertNewGridItem
|
||||
} from '../appUtils'
|
||||
import type { AppEditorContext, AppViewerContext, EditorBreakpoint, GridItem } from '../../types'
|
||||
import { findGridItemParentId } from '../appUtils'
|
||||
import { push } from '$lib/history'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
|
||||
const { app, selectedComponent, breakpoint, focusedGrid } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { componentControl, history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let tempGridItem: GridItem | undefined = undefined
|
||||
let copiedGridItem: GridItem | undefined = undefined
|
||||
|
||||
function getSortedGridItemsOfChildren(): GridItem[] {
|
||||
if (!$focusedGrid) {
|
||||
return sortGridItems($app.grid, $breakpoint)
|
||||
return sortGridItemsPosition($app.grid, $breakpoint)
|
||||
}
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sortGridItems(
|
||||
return sortGridItemsPosition(
|
||||
$app.subgrids[`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`],
|
||||
$breakpoint
|
||||
)
|
||||
@@ -25,14 +37,14 @@
|
||||
|
||||
function getSortedGridItemsOfGrid(): GridItem[] {
|
||||
if ($app.grid.find((item) => item.id === $selectedComponent)) {
|
||||
return sortGridItems($app.grid, $breakpoint)
|
||||
return sortGridItemsPosition($app.grid, $breakpoint)
|
||||
}
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sortGridItems(
|
||||
return sortGridItemsPosition(
|
||||
Object.values($app.subgrids ?? {}).find((grid) =>
|
||||
grid.find((item) => item.id === $selectedComponent)
|
||||
) ?? [],
|
||||
@@ -79,7 +91,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const sortedGridItems = sortGridItems(subgrid, $breakpoint)
|
||||
const sortedGridItems = sortGridItemsPosition(subgrid, $breakpoint)
|
||||
|
||||
if (sortedGridItems) {
|
||||
$selectedComponent = sortedGridItems[0].id
|
||||
@@ -88,23 +100,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleArrowUp(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) return
|
||||
let parentId = findGridItemParentGrid($app, $selectedComponent)?.split('-')[0]
|
||||
if (parentId) {
|
||||
$selectedComponent = parentId
|
||||
} else {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) {
|
||||
return
|
||||
}
|
||||
tempGridItem = undefined
|
||||
copiedGridItem = findGridItem($app, $selectedComponent)
|
||||
}
|
||||
|
||||
function handleCut(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) {
|
||||
return
|
||||
}
|
||||
push(history, $app)
|
||||
|
||||
const gridItem = findGridItem($app, $selectedComponent)
|
||||
copiedGridItem = gridItem
|
||||
|
||||
if (!gridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the grid item in a temp variable so we can paste it later
|
||||
tempGridItem = gridItem
|
||||
}
|
||||
|
||||
function handlePaste(event: KeyboardEvent) {
|
||||
push(history, $app)
|
||||
|
||||
if (tempGridItem) {
|
||||
if (
|
||||
$focusedGrid &&
|
||||
getAllSubgridsAndComponentIds($app, tempGridItem.data)[0].includes(
|
||||
`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`
|
||||
)
|
||||
) {
|
||||
sendUserToast('Cannot paste a component into itself', true)
|
||||
return
|
||||
}
|
||||
let parentGrid = findGridItemParentGrid($app, tempGridItem.data.id)
|
||||
const grid = parentGrid ? $app.subgrids![parentGrid] : $app.grid
|
||||
let idx = grid.findIndex((item) => {
|
||||
return item.id == tempGridItem!.data.id
|
||||
})
|
||||
if (idx > -1) {
|
||||
grid.splice(idx, 1)
|
||||
}
|
||||
|
||||
insertNewGridItem($app, tempGridItem.data, $focusedGrid, true)
|
||||
copiedGridItem = tempGridItem
|
||||
tempGridItem = undefined
|
||||
} else if (copiedGridItem) {
|
||||
insertNewGridItem($app, copiedGridItem.data, $focusedGrid, false)
|
||||
}
|
||||
|
||||
$app = $app
|
||||
}
|
||||
|
||||
function keydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
$selectedComponent = undefined
|
||||
event.preventDefault()
|
||||
handleEscape(event)
|
||||
break
|
||||
|
||||
case 'ArrowUp': {
|
||||
if (!$selectedComponent) return
|
||||
let parentId = findGridItemParentId($app, $selectedComponent)
|
||||
if (parentId) {
|
||||
$selectedComponent = parentId
|
||||
} else {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
break
|
||||
handleArrowUp(event)
|
||||
}
|
||||
|
||||
case 'ArrowDown': {
|
||||
@@ -122,12 +199,30 @@
|
||||
break
|
||||
}
|
||||
|
||||
case 'c':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handleCopy(event)
|
||||
}
|
||||
break
|
||||
|
||||
case 'v':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handlePaste(event)
|
||||
}
|
||||
break
|
||||
|
||||
case 'x':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handleCut(event)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function sortGridItems(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
|
||||
function sortGridItemsPosition(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
|
||||
return gridItems.sort((a: GridItem, b: GridItem) => {
|
||||
const width = breakpoint === 'lg' ? 12 : 3
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { fly } from 'svelte/transition'
|
||||
import Editor from '$lib/components/Editor.svelte'
|
||||
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
|
||||
import Popover from '../../../Popover.svelte'
|
||||
|
||||
@@ -19,14 +19,12 @@
|
||||
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
|
||||
import type { AppComponent } from '../component'
|
||||
import CssProperty from '../componentsPanel/CssProperty.svelte'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import GridTab from './GridTab.svelte'
|
||||
import { duplicateGridItem } from '../appUtils'
|
||||
import { deleteGridItem } from '../appUtils'
|
||||
import MoveToOtherGrid from './MoveToOtherGrid.svelte'
|
||||
import GridPane from './GridPane.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { push } from '$lib/history'
|
||||
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let rowColumns = false
|
||||
@@ -47,13 +45,6 @@
|
||||
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function duplicateElement(id: string) {
|
||||
push(history, $app)
|
||||
$dirtyStore = true
|
||||
const newId = duplicateGridItem($app, parent, id)
|
||||
$selectedComponent = newId
|
||||
}
|
||||
|
||||
function removeGridElement() {
|
||||
push(history, $app)
|
||||
$selectedComponent = undefined
|
||||
@@ -96,7 +87,7 @@
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
{#if component}
|
||||
<div class="flex flex-col min-w-[150px] w-full divide-y">
|
||||
<div class="flex min-h-full flex-col min-w-[150px] w-full divide-y">
|
||||
{#if component.componentInput}
|
||||
<PanelSection
|
||||
smallPadding
|
||||
@@ -195,6 +186,8 @@
|
||||
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
|
||||
{/if}
|
||||
|
||||
<div class="grow shrink" />
|
||||
|
||||
{#if Object.keys(component.customCss ?? {}).length > 0}
|
||||
<PanelSection title="Custom CSS">
|
||||
<div slot="action">
|
||||
@@ -222,8 +215,20 @@
|
||||
{/if}
|
||||
|
||||
{#if duplicateMoveAllowed}
|
||||
<PanelSection title="Duplicate">
|
||||
<Button
|
||||
<PanelSection title="Copy/Move">
|
||||
<div slot="action">
|
||||
<Button size="xs" color="red" variant="border" on:click={removeGridElement}>
|
||||
Delete
|
||||
<Kbd>Del</Kbd>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
><span class="text-gray-600 text-xs mr-2">Copy:</span><Kbd>CTRL+C</Kbd> + <Kbd
|
||||
>CTRL+V</Kbd
|
||||
></div
|
||||
>
|
||||
<!-- <Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
startIcon={{ icon: faCopy }}
|
||||
@@ -234,24 +239,19 @@
|
||||
}}
|
||||
>
|
||||
Duplicate component: {component.id}
|
||||
</Button>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Move to other grid">
|
||||
<MoveToOtherGrid bind:component {parent} />
|
||||
</Button> -->
|
||||
<div
|
||||
><span class="text-gray-600 text-xs mr-2">Move:</span><Kbd>CTRL+X</Kbd> + <Kbd
|
||||
>CTRL+V</Kbd
|
||||
></div
|
||||
>
|
||||
<div
|
||||
><span class="text-gray-600 text-xs mr-2">Navigate:</span><Kbd>←</Kbd><Kbd
|
||||
>↑</Kbd
|
||||
><Kbd>→</Kbd><Kbd>ESC</Kbd></div
|
||||
>
|
||||
</div>
|
||||
</PanelSection>
|
||||
{/if}
|
||||
|
||||
<PanelSection title="Danger zone">
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
on:click={removeGridElement}
|
||||
>
|
||||
Delete component: {component.id}
|
||||
</Button>
|
||||
</PanelSection>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import CloseButton from '$lib/components/common/CloseButton.svelte'
|
||||
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { CrossIcon } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { deleteGridItem } from '../appUtils'
|
||||
@@ -56,18 +58,8 @@
|
||||
<div class="w-full flex flex-row gap-2 items-center relative">
|
||||
<input on:keydown|stopPropagation type="text" bind:value />
|
||||
|
||||
<div class="absolute top-1 right-1">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
deleteSubgrid(index)
|
||||
}}
|
||||
iconOnly
|
||||
btnClasses="!text-red-500"
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
/>
|
||||
<div class="absolute right-1">
|
||||
<CloseButton noBg on:close={() => deleteSubgrid(index)} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import { push } from '$lib/history'
|
||||
import { faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { App, AppEditorContext, AppViewerContext } from '../../types'
|
||||
import {
|
||||
createNewGridItem,
|
||||
deleteGridItem,
|
||||
findGridItem,
|
||||
getAllSubgridsAndComponentIds,
|
||||
insertNewGridItem
|
||||
} from '../appUtils'
|
||||
import type { AppComponent } from '../component'
|
||||
|
||||
export let component: AppComponent | undefined
|
||||
export let parent: string | undefined
|
||||
|
||||
let selectedOption: string
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function listAllSubGrids(app: App) {
|
||||
return app.subgrids ? Object.keys(app.subgrids) : []
|
||||
}
|
||||
|
||||
function findAndDelete(id: string) {
|
||||
const node = findGridItem($app, id)
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
const data: AppComponent = JSON.parse(JSON.stringify(node.data))
|
||||
const shouldKeepSubGrid = data.numberOfSubgrids ? data.numberOfSubgrids >= 1 : false
|
||||
|
||||
$dirtyStore = true
|
||||
|
||||
deleteGridItem($app, data, parent, shouldKeepSubGrid)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function insertComponentInSubGrid(
|
||||
data: AppComponent,
|
||||
targetId: string,
|
||||
targetSubGridIndex: number
|
||||
) {
|
||||
push(history, $app)
|
||||
const focusedGrid = {
|
||||
parentComponentId: targetId,
|
||||
subGridIndex: targetSubGridIndex
|
||||
}
|
||||
|
||||
insertNewGridItem($app, data, focusedGrid, true)
|
||||
|
||||
$app.grid = [...$app.grid]
|
||||
}
|
||||
|
||||
function insertComponentInMainGrid(data: AppComponent) {
|
||||
const newComponent = createNewGridItem($app.grid, data.id, data)
|
||||
$app.grid = [...$app.grid, newComponent]
|
||||
}
|
||||
|
||||
function onMove(component: AppComponent) {
|
||||
push(history, $app)
|
||||
const data = findAndDelete(component.id)
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedOption !== 'main-grid') {
|
||||
const [targetId, targetSubGridIndex] = selectedOption.split('-')
|
||||
insertComponentInSubGrid(data, targetId, parseInt(targetSubGridIndex))
|
||||
} else {
|
||||
insertComponentInMainGrid(data)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultOption = {
|
||||
label: 'Main grid',
|
||||
value: 'main-grid',
|
||||
disabled: parent === undefined
|
||||
}
|
||||
|
||||
$: [subgrids] = component ? getAllSubgridsAndComponentIds($app, component) : [[], []]
|
||||
$: availableGrids = listAllSubGrids($app)
|
||||
$: options = availableGrids
|
||||
? [
|
||||
defaultOption,
|
||||
...availableGrids?.map((grid) => ({
|
||||
label: grid,
|
||||
value: grid,
|
||||
disabled: grid === parent || subgrids.includes(grid)
|
||||
}))
|
||||
]
|
||||
: [defaultOption]
|
||||
</script>
|
||||
|
||||
{#if component && !options.every((option) => option.disabled)}
|
||||
<select bind:value={selectedOption} class="w-full">
|
||||
{#each options as option}
|
||||
<option value={option.value} disabled={option.disabled}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
startIcon={{ icon: faCopy }}
|
||||
on:click={() => {
|
||||
if (component) {
|
||||
onMove(component)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Move component: {component.id}
|
||||
</Button>
|
||||
{:else}
|
||||
<p class="text-gray-500 text-sm">No grids available</p>
|
||||
{/if}
|
||||
18
frontend/src/lib/components/common/CloseButton.svelte
Normal file
18
frontend/src/lib/components/common/CloseButton.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import { faClose } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { Icon } from 'svelte-awesome'
|
||||
|
||||
export let noBg = false
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={() => dispatch('close')}
|
||||
class="hover:bg-gray-200 {noBg
|
||||
? ''
|
||||
: 'bg-gray-100'} rounded-full w-8 h-8 flex items-center justify-center transition-all"
|
||||
>
|
||||
<Icon data={faClose} class="text-gray-500" />
|
||||
</button>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { faClose } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import CloseButton from '../CloseButton.svelte'
|
||||
|
||||
export let title: string | undefined = undefined
|
||||
export let overflow_y = true
|
||||
@@ -15,12 +16,7 @@
|
||||
<div class="flex flex-col divide-y h-screen max-h-screen">
|
||||
<div class="flex justify-between w-wull items-center px-2 py-2 ">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
on:click={() => dispatch('close')}
|
||||
class="hover:bg-gray-200 bg-gray-100 rounded-full w-8 h-8 flex items-center justify-center transition-all"
|
||||
>
|
||||
<Icon data={faClose} class="text-gray-500" />
|
||||
</button>
|
||||
<CloseButton on:close />
|
||||
|
||||
<span class="font-semibold truncate text-gray-800">{title}</span>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
)}
|
||||
style={`${style} ${$selected?.startsWith(value) ? selectedStyle : ''}`}
|
||||
on:click={() => update(value)}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
undefined,
|
||||
1000,
|
||||
loopDepth,
|
||||
0,
|
||||
modules.findIndex((m) => m.id == module.id) + 1,
|
||||
true,
|
||||
undefined,
|
||||
module.id
|
||||
|
||||
Reference in New Issue
Block a user