feat(frontend): Duplicate component (#1228)

* feat(frontend): Duplicate component

* feat(frontend): add missing types
This commit is contained in:
Faton Ramadani
2023-02-23 17:43:27 +01:00
committed by GitHub
parent 21f3f09dcd
commit 5b1673eceb
3 changed files with 169 additions and 118 deletions

View File

@@ -1,134 +1,32 @@
<script lang="ts">
import type { AppEditorContext, GridItem } from '../../types'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import { getContext, onMount } from 'svelte'
import { getNextId } from '$lib/components/flows/flowStateUtils'
import { isOpenStore } from './store'
import { gridColumns } from '../../gridUtils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import {
components as componentsRecord,
COMPONENT_SETS,
getRecommendedDimensionsByComponent,
type AppComponent
} from '../component'
import { components as componentsRecord, COMPONENT_SETS, type AppComponent } from '../component'
import ListItem from './ListItem.svelte'
import { insertNewGridItem, createNewGridItem, getNextGridItemId } from '../../utils'
const TITLE_PREFIX = 'Component.' as const
const { app, selectedComponent, focusedGrid } = getContext<AppEditorContext>('AppEditorContext')
// The grid is needed to find a space for the new component
function createNewGridItem(grid: GridItem[], id: string, appComponentType: string): GridItem {
const appComponent = componentsRecord[appComponentType].data
appComponent.id = id
const newComponent = {
fixed: false,
resizable: true,
draggable: true,
customDragger: false,
customResizer: false,
x: 0,
y: 0
}
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
const newItem: GridItem = {
data: newData,
id: id
}
gridColumns.forEach((column) => {
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
newItem[column] = {
...newComponent,
min: { w: 1, h: 1 },
max: { w: column, h: 100 },
w: rec.w,
h: rec.h
}
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
newItem[column] = { ...newItem[column], ...position }
})
return newItem
}
function recursiveGetIds(gridItem: GridItem): string[] {
const subGrids = gridItem.data.subGrids ?? []
const subGridIds = subGrids
.map((subGrid: GridItem[]) => subGrid.map(recursiveGetIds))
.flat(Infinity)
return [gridItem.data.id, ...subGridIds]
}
function findParent(root: GridItem[], id: string): GridItem | undefined {
for (const a of root) {
if (a.id === id) {
return a
}
if (a.data.subGrids) {
// Recursively search the sub-grids
for (const subGrid of a.data.subGrids) {
const result = findParent(subGrid, id)
if (result) {
return result
}
}
}
}
return undefined
}
function insertNewGridItem(
root: GridItem[],
id: string,
subGridIndex: number,
appComponentType: string,
newId: string
): GridItem[] {
const parentA = findParent(root, id)
if (!parentA) {
throw new Error(`Parent A object with ID ${id} not found.`)
}
const subGrid = parentA.data.subGrids[subGridIndex]
if (!subGrid) {
throw new Error(`Sub-grid with index ${subGridIndex} not found.`)
}
const newItem = createNewGridItem(subGrid ?? [], newId, appComponentType)
subGrid.push(newItem)
return root
}
function addComponent(appComponentType: AppComponent['type']): void {
// When a new component is added, we need to mark the app as dirty,
// so a confirmation modal will appear if the user tries to leave the page
$dirtyStore = true
const grid = $app.grid ?? []
const gridItemIds = grid.map(recursiveGetIds).flat()
const id = getNextId(gridItemIds)
const id = getNextGridItemId(grid)
const data = componentsRecord[appComponentType].data
if ($focusedGrid) {
const { parentComponentId, subGridIndex } = $focusedGrid
$app.grid = insertNewGridItem(
$app.grid,
parentComponentId,
subGridIndex,
appComponentType,
id
)
$app.grid = insertNewGridItem($app.grid, parentComponentId, subGridIndex, id, data)
} else {
const newItem = createNewGridItem(grid, id, appComponentType)
const newItem = createNewGridItem(grid, id, data)
$app.grid = [...grid, newItem]
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { faCopy, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import PanelSection from './common/PanelSection.svelte'
@@ -10,7 +10,14 @@
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { capitalize, classNames } from '$lib/utils'
import { buildExtraLib, fieldTypeToTsType } from '../../utils'
import {
buildExtraLib,
createNewGridItem,
fieldTypeToTsType,
findParent,
getNextGridItemId,
insertNewGridItem
} from '../../utils'
import Recompute from './Recompute.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import ComponentInputTypeEditor from './ComponentInputTypeEditor.svelte'
@@ -19,14 +26,40 @@
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'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
export let rowColumns = false
const { app, staticOutputs, runnableComponents, selectedComponent, worldStore } =
const { app, staticOutputs, runnableComponents, selectedComponent, worldStore, focusedGrid } =
getContext<AppEditorContext>('AppEditorContext')
function duplicateElement(id: string) {
const parent = findParent($app.grid, id)
if (!parent) {
return
}
const data: AppComponent = JSON.parse(JSON.stringify(parent.data))
$dirtyStore = true
const grid = $app.grid ?? []
const newId = getNextGridItemId(grid)
if ($focusedGrid) {
const { parentComponentId, subGridIndex } = $focusedGrid
$app.grid = insertNewGridItem($app.grid, parentComponentId, subGridIndex, newId, data)
} else {
const newItem = createNewGridItem(grid, newId, data)
$app.grid = [...grid, newItem]
}
$selectedComponent = newId
}
function removeGridElement() {
$selectedComponent = undefined
if (onDelete && component) {
@@ -192,6 +225,22 @@
</PanelSection>
{/if}
<PanelSection title="Duplicate">
<Button
size="xs"
color="blue"
variant="border"
startIcon={{ icon: faCopy }}
on:click={() => {
if (component) {
duplicateElement(component.id)
}
}}
>
Duplicate component: {component.id}
</Button>
</PanelSection>
<PanelSection title="Danger zone">
<Button
size="xs"

View File

@@ -2,11 +2,19 @@ import type { Schema } from '$lib/common'
import { FlowService, ScriptService } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { emptySchema } from '$lib/utils'
import type { AppComponent } from './editor/component'
import type { AppComponent, AppComponentConfig } from './editor/component'
import {
components as componentsRecord,
getRecommendedDimensionsByComponent
} from './editor/component'
import { gridColumns } from './gridUtils'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
import type { Output } from './rx'
import type { App, GridItem } from './types'
import { getNextId } from '../flows/flowStateUtils'
export async function loadSchema(
workspace: string,
@@ -65,8 +73,6 @@ export function schemaToInputsSpec(
}, {})
}
export function accessPropertyByPath<T>(object: T, path: string): T | undefined {
// convert indexes to properties
path = path.replace(/\[(\w+)\]/g, '.$1')
@@ -202,13 +208,111 @@ export function getAllScriptNames(app: App): string[] {
}
function clearAndUpper(text: string) {
return text.replace(/-/, "").toUpperCase();
return text.replace(/-/, '').toUpperCase()
}
export function toPascalCase(text: string) {
return text.replace(/(^\w|-\w)/g, clearAndUpper);
return text.replace(/(^\w|-\w)/g, clearAndUpper)
}
export function toKebabCase(text: string) {
return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
return text.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
}
export function findParent(root: GridItem[], id: string): GridItem | undefined {
for (const a of root) {
if (a.id === id) {
return a
}
if (a.data.subGrids) {
// Recursively search the sub-grids
for (const subGrid of a.data.subGrids) {
const result = findParent(subGrid, id)
if (result) {
return result
}
}
}
}
return undefined
}
export function insertNewGridItem(
root: GridItem[],
id: string,
subGridIndex: number,
newId: string,
data: AppComponent
): GridItem[] {
const parentA = findParent(root, id)
if (!parentA) {
throw new Error(`Parent A object with ID ${id} not found.`)
}
const subGrid = parentA.data.subGrids[subGridIndex]
if (!subGrid) {
throw new Error(`Sub-grid with index ${subGridIndex} not found.`)
}
const newItem = createNewGridItem(subGrid ?? [], newId, data)
subGrid.push(newItem)
return root
}
// The grid is needed to find a space for the new component
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const appComponent = data
appComponent.id = id
const newComponent = {
fixed: false,
resizable: true,
draggable: true,
customDragger: false,
customResizer: false,
x: 0,
y: 0
}
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
const newItem: GridItem = {
data: newData,
id: id
}
gridColumns.forEach((column) => {
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
newItem[column] = {
...newComponent,
min: { w: 1, h: 1 },
max: { w: column, h: 100 },
w: rec.w,
h: rec.h
}
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
newItem[column] = { ...newItem[column], ...position }
})
return newItem
}
function recursiveGetIds(gridItem: GridItem): string[] {
const subGrids = gridItem.data.subGrids ?? []
const subGridIds = subGrids
.map((subGrid: GridItem[]) => subGrid.map(recursiveGetIds))
.flat(Infinity)
return [gridItem.data.id, ...subGridIds]
}
export function getNextGridItemId(grid: GridItem[] = []): string {
const gridItemIds = grid.map(recursiveGetIds).flat()
const id = getNextId(gridItemIds)
return id
}