* Add animation on connection plugs * Remove connection pannel * Modify click outside * fix minor issue * remove unused component * Prevent keyboard component navigation when connecting * fix left right panel inversion * fix pannel logic * close secondary menu on connection * Adjust plug position according to id badge size * Change components colors * Change plug color * Fix escape connection * Change cursor when connecting * Add user toast on connecting * Add component to exclusion area in connection mode * exit connection on click connection button * create connection button component * remove debug logs * fix pen color * polish * Add alert message in connection mode * fix minor issue * fix unwanted cursor override
1284 lines
31 KiB
TypeScript
1284 lines
31 KiB
TypeScript
import type {
|
|
App,
|
|
BaseAppComponent,
|
|
ComponentCustomCSS,
|
|
ConnectingInput,
|
|
EditorBreakpoint,
|
|
FocusedGrid,
|
|
GeneralAppInput,
|
|
GridItem
|
|
} from '../types'
|
|
import {
|
|
ccomponents,
|
|
components,
|
|
getRecommendedDimensionsByComponent,
|
|
type AppComponent,
|
|
type BaseComponent,
|
|
type InitialAppComponent,
|
|
type TypedComponent
|
|
} from './component'
|
|
import { gridColumns } from '../gridUtils'
|
|
import { allItems } from '../utils'
|
|
import type { Output, World } from '../rx'
|
|
import type { FilledItem, Size } from '../svelte-grid/types'
|
|
import type {
|
|
StaticAppInput,
|
|
EvalAppInput,
|
|
EvalV2AppInput,
|
|
InputConnectionEval,
|
|
StaticAppInputOnDemand,
|
|
AppInputs
|
|
} from '../inputType'
|
|
import { get, type Writable } from 'svelte/store'
|
|
import { deepMergeWithPriority } from '$lib/utils'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { getNextId } from '$lib/components/flows/idUtils'
|
|
import { enterpriseLicense } from '$lib/stores'
|
|
import gridHelp from '../svelte-grid/utils/helper'
|
|
|
|
export function findComponentSettings(app: App, id: string | undefined) {
|
|
if (!id) return undefined
|
|
if (app?.grid) {
|
|
const gridItem = app.grid.find((x) => x.data?.id === id)
|
|
if (gridItem) {
|
|
return { item: gridItem, parent: undefined }
|
|
}
|
|
}
|
|
|
|
if (app?.subgrids) {
|
|
for (const key of Object.keys(app.subgrids ?? {})) {
|
|
const gridItem = app.subgrids[key].find((x) => x.data?.id === id)
|
|
if (gridItem) {
|
|
return { item: gridItem, parent: key }
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
export function dfs(
|
|
grid: GridItem[],
|
|
id: string,
|
|
subgrids: Record<string, GridItem[]>
|
|
): string[] | undefined {
|
|
if (!grid) {
|
|
return undefined
|
|
}
|
|
for (const item of grid) {
|
|
if (item.id === id) {
|
|
return [id]
|
|
} else if (
|
|
item.data.type == 'tablecomponent' &&
|
|
item.data.actionButtons.find((x) => id == x.id)
|
|
) {
|
|
return [item.id, id]
|
|
} else if (item.data.type == 'menucomponent' && item.data.menuItems.find((x) => id == x.id)) {
|
|
return [item.id, id]
|
|
} else if (
|
|
(item.data.type == 'aggridcomponent' ||
|
|
item.data.type == 'aggridcomponentee' ||
|
|
item.data.type == 'dbexplorercomponent' ||
|
|
item.data.type == 'aggridinfinitecomponent' ||
|
|
item.data.type == 'aggridinfinitecomponentee') &&
|
|
item.data.actions?.find((x) => id == x.id)
|
|
) {
|
|
return [item.id, id]
|
|
} else {
|
|
for (let i = 0; i < (item.data.numberOfSubgrids ?? 0); i++) {
|
|
const res = dfs(subgrids[`${item.id}-${i}`], id, subgrids)
|
|
if (res) {
|
|
return [item.id, ...res]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function selectId(
|
|
e: PointerEvent,
|
|
id: string,
|
|
selectedComponent: Writable<string[] | undefined>,
|
|
app: App
|
|
) {
|
|
;(document?.activeElement as HTMLElement)?.blur()
|
|
if (e.shiftKey) {
|
|
selectedComponent.update((old) => {
|
|
if (old && old?.[0]) {
|
|
if (findGridItemParentGrid(app, old[0]) != findGridItemParentGrid(app, id)) {
|
|
sendUserToast('Cannot multi select items from different grids', true)
|
|
return old
|
|
}
|
|
}
|
|
if (old == undefined) {
|
|
return [id]
|
|
}
|
|
if (old.includes(id)) {
|
|
return old
|
|
}
|
|
return [...old, id]
|
|
})
|
|
} else {
|
|
if (get(selectedComponent)?.includes(id)) {
|
|
return
|
|
} else {
|
|
selectedComponent.set([id])
|
|
}
|
|
}
|
|
}
|
|
|
|
export function connectOutput(
|
|
connectingInput: Writable<ConnectingInput>,
|
|
typ: TypedComponent['type'],
|
|
id: string,
|
|
spath: string
|
|
) {
|
|
if (get(connectingInput).opened) {
|
|
let splitted = spath?.split('.')
|
|
let componentId = typ == 'containercomponent' ? splitted?.[0] : id
|
|
let path = typ == 'containercomponent' ? splitted?.[1] : spath
|
|
connectingInput.set(connectInput(get(connectingInput), componentId, path, typ))
|
|
}
|
|
}
|
|
function findGridItemById(
|
|
root: GridItem[],
|
|
subGrids: Record<string, GridItem[]> | undefined,
|
|
id: string
|
|
): GridItem | undefined {
|
|
for (const gridItem of allItems(root, subGrids)) {
|
|
if (gridItem.id === id) {
|
|
return gridItem
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function findGridItemParentGrid(app: App, id: string): string | undefined {
|
|
const gridItem = app.grid.find((x) => x.id === id)
|
|
if (gridItem || app.subgrids === undefined) {
|
|
return undefined
|
|
} else {
|
|
for (const key of Object.keys(app.subgrids ?? {})) {
|
|
const subGrid = app.subgrids[key]
|
|
const gridItemIdx = subGrid.findIndex((x) => x.id === id)
|
|
if (gridItemIdx > -1) {
|
|
return key
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function allsubIds(app: App, parentId: string): string[] {
|
|
let item = findGridItem(app, parentId)
|
|
if (!item?.data.numberOfSubgrids) {
|
|
let subIds = [parentId]
|
|
if (item) {
|
|
if (item.data.type === 'tablecomponent') {
|
|
subIds.push(...item.data.actionButtons?.map((x) => x.id))
|
|
}
|
|
if (
|
|
(item.data.type === 'aggridcomponent' ||
|
|
item.data.type === 'aggridcomponentee' ||
|
|
item.data.type === 'dbexplorercomponent' ||
|
|
item.data.type === 'aggridinfinitecomponent' ||
|
|
item.data.type === 'aggridinfinitecomponentee') &&
|
|
Array.isArray(item.data.actions)
|
|
) {
|
|
subIds.push(...item.data.actions?.map((x) => x.id))
|
|
}
|
|
if (item.data.type === 'menucomponent') {
|
|
subIds.push(...item.data.menuItems?.map((x) => x.id))
|
|
}
|
|
}
|
|
return subIds
|
|
}
|
|
return getAllSubgridsAndComponentIds(app, item?.data)[1]
|
|
}
|
|
|
|
export function findGridItem(app: App, id: string): GridItem | undefined {
|
|
return findGridItemById(app.grid, app.subgrids, id)
|
|
}
|
|
|
|
export function getNextGridItemId(app: App): string {
|
|
const allIds = allItems(app.grid, app.subgrids).flatMap((x) => {
|
|
if (x.data.type === 'tablecomponent') {
|
|
return [x.id, ...x.data.actionButtons.map((x) => x.id)]
|
|
} else if (
|
|
(x.data.type === 'aggridcomponent' ||
|
|
x.data.type === 'aggridcomponentee' ||
|
|
x.data.type === 'dbexplorercomponent' ||
|
|
x.data.type === 'aggridinfinitecomponent' ||
|
|
x.data.type === 'aggridinfinitecomponentee') &&
|
|
Array.isArray(x.data.actions)
|
|
) {
|
|
return [x.id, ...x.data.actions.map((x) => x.id)]
|
|
} else if (x.data.type === 'menucomponent') {
|
|
return [x.id, ...x.data.menuItems.map((x) => x.id)]
|
|
} else {
|
|
return [x.id]
|
|
}
|
|
})
|
|
return getNextId(allIds)
|
|
}
|
|
|
|
export function getAllRecomputeIdsForComponent(app: App, id: string | undefined) {
|
|
if (!app || !id) return []
|
|
const items = allItems(app.grid, app.subgrids)
|
|
|
|
const recomputedBy: string[] = []
|
|
|
|
items.forEach((item) => {
|
|
if (item.data.type === 'buttoncomponent') {
|
|
if (item.data.recomputeIds?.includes(id)) {
|
|
recomputedBy.push(item.id)
|
|
}
|
|
}
|
|
})
|
|
|
|
return recomputedBy
|
|
}
|
|
|
|
export function createNewGridItem(
|
|
grid: GridItem[],
|
|
id: string,
|
|
data: AppComponent,
|
|
columns?: Record<number, any>,
|
|
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
|
recOverride?: Record<number, Size>,
|
|
fixed?: boolean,
|
|
shouldNotFindSpace?: boolean
|
|
): GridItem {
|
|
const newComponent = {
|
|
fixed: fixed ?? false,
|
|
x: initialPosition.x,
|
|
y: initialPosition.y,
|
|
fullHeight: false
|
|
}
|
|
|
|
let newData: AppComponent = JSON.parse(JSON.stringify(data))
|
|
newData.id = id
|
|
|
|
const newItem: GridItem = {
|
|
data: newData,
|
|
id: id
|
|
}
|
|
|
|
gridColumns.forEach((column) => {
|
|
if (!columns) {
|
|
const rec = recOverride?.[column] ?? getRecommendedDimensionsByComponent(newData.type, column)
|
|
|
|
newItem[column] = {
|
|
...newComponent,
|
|
w: rec.w,
|
|
h: rec.h
|
|
}
|
|
} else {
|
|
newItem[column] = {
|
|
...columns[column],
|
|
x: initialPosition.x,
|
|
y: initialPosition.y
|
|
}
|
|
}
|
|
|
|
let shouldComputePosition: boolean = false
|
|
|
|
// Fallback to avoid component disapearing
|
|
if (initialPosition.x === undefined || initialPosition.y === undefined) {
|
|
newItem[column].x = 0
|
|
newItem[column].y = 0
|
|
shouldComputePosition = true
|
|
}
|
|
|
|
// Either the final position is controlled using initialPosition or the position is computed because the component positions are wrong
|
|
if (!shouldNotFindSpace || shouldComputePosition) {
|
|
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
|
|
|
|
newItem[column] = { ...newItem[column], ...position }
|
|
}
|
|
})
|
|
|
|
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] ?? []
|
|
}
|
|
}
|
|
|
|
function cleanseValue(
|
|
key: string,
|
|
value: {
|
|
type: 'eval' | 'static' | 'evalv2'
|
|
value?: any
|
|
expr?: string
|
|
connections?: InputConnectionEval[]
|
|
}
|
|
) {
|
|
if (!value) {
|
|
return [key, undefined]
|
|
}
|
|
if (value.type === 'static') {
|
|
return [key, { type: value.type, value: value.value }]
|
|
} else if (value.type === 'eval') {
|
|
return [key, { type: value.type, expr: value.expr }]
|
|
} else {
|
|
return [key, { type: value.type, expr: value.expr, connections: value.connections }]
|
|
}
|
|
}
|
|
|
|
export function cleanseOneOfConfiguration(
|
|
configuration: Record<
|
|
string,
|
|
Record<string, GeneralAppInput & (StaticAppInput | EvalAppInput | EvalV2AppInput)>
|
|
>
|
|
) {
|
|
return Object.fromEntries(
|
|
Object.entries(configuration).map(([key, val]) => [
|
|
key,
|
|
Object.fromEntries(Object.entries(val).map(([key, val]) => cleanseValue(key, val)))
|
|
])
|
|
)
|
|
}
|
|
|
|
export function appComponentFromType<T extends keyof typeof components>(
|
|
type: T,
|
|
overrideConfiguration?: Partial<InitialAppComponent['configuration']>,
|
|
extra?: any,
|
|
override?: {
|
|
customCss?: ComponentCustomCSS<T>
|
|
verticalAlignment?: 'top' | 'center' | 'bottom'
|
|
horizontalAlignment?: 'left' | 'center' | 'right'
|
|
componnetInput?: Partial<InitialAppComponent['componentInput']>
|
|
}
|
|
): (id: string) => BaseAppComponent & BaseComponent<T> {
|
|
return (id: string) => {
|
|
const init = JSON.parse(JSON.stringify(ccomponents[type].initialData)) as InitialAppComponent
|
|
|
|
const configuration = Object.fromEntries(
|
|
Object.entries(init.configuration).map(([key, value]) => {
|
|
if (value.type != 'oneOf') {
|
|
return cleanseValue(key, value)
|
|
} else {
|
|
return [
|
|
key,
|
|
{
|
|
type: value.type,
|
|
selected: value.selected,
|
|
configuration: cleanseOneOfConfiguration(value.configuration)
|
|
}
|
|
]
|
|
}
|
|
})
|
|
)
|
|
|
|
return {
|
|
type,
|
|
//TODO remove tooltip from there
|
|
configuration: deepMergeWithPriority(configuration, overrideConfiguration ?? {}),
|
|
componentInput: override?.componnetInput ?? init.componentInput,
|
|
panes: init.panes,
|
|
tabs: init.tabs,
|
|
conditions: init.conditions,
|
|
nodes: init.nodes,
|
|
customCss: deepMergeWithPriority(
|
|
ccomponents[type].customCss as any,
|
|
override?.customCss ?? {}
|
|
),
|
|
recomputeIds: init.recomputeIds ? [] : undefined,
|
|
actionButtons: init.actionButtons ? [] : undefined,
|
|
actions: [],
|
|
menuItems: init.menuItems ? [] : undefined,
|
|
numberOfSubgrids: init.numberOfSubgrids,
|
|
horizontalAlignment: override?.horizontalAlignment ?? init.horizontalAlignment,
|
|
verticalAlignment: override?.verticalAlignment ?? init.verticalAlignment,
|
|
id,
|
|
...(extra ?? {})
|
|
}
|
|
}
|
|
}
|
|
export function insertNewGridItem(
|
|
app: App,
|
|
builddata: (id: string) => AppComponent,
|
|
focusedGrid: FocusedGrid | undefined,
|
|
columns?: Record<string, any>,
|
|
keepId?: string,
|
|
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
|
recOverride?: Record<number, Size>,
|
|
keepSubgrids?: boolean,
|
|
fixed?: boolean,
|
|
shouldNotFindSpace?: boolean
|
|
): string {
|
|
const id = keepId ?? getNextGridItemId(app)
|
|
|
|
const data = builddata(id)
|
|
|
|
if (data.type == 'aggridcomponentee' && !get(enterpriseLicense)) {
|
|
sendUserToast('AgGrid Enterprise Edition require Windmill Enterprise Edition', true)
|
|
throw Error('AgGrid Enterprise Edition require Windmill Enterprise Edition')
|
|
}
|
|
if (!app.subgrids) {
|
|
app.subgrids = {}
|
|
}
|
|
|
|
// We only want to set subgrids when we are not moving
|
|
if (!keepId || keepSubgrids) {
|
|
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
|
|
app.subgrids[`${id}-${i}`] = []
|
|
}
|
|
}
|
|
|
|
const key = focusedGrid
|
|
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
|
|
: undefined
|
|
|
|
if (key && app.subgrids[key] === undefined) {
|
|
let parent = findGridItemById(app.grid, app.subgrids, key)?.data
|
|
let subgrids = parent?.numberOfSubgrids
|
|
if (subgrids === undefined) {
|
|
throw Error(
|
|
`Invalid subgrid selected, the parent has no subgrids: ${key}, parent: ${JSON.stringify(
|
|
parent
|
|
)}`
|
|
)
|
|
}
|
|
if (
|
|
focusedGrid?.subGridIndex &&
|
|
(focusedGrid?.subGridIndex < 0 || focusedGrid?.subGridIndex >= subgrids)
|
|
) {
|
|
throw Error(`Invalid subgrid selected: ${key}, max subgrids: ${subgrids}`)
|
|
}
|
|
// If ever the subgrid is undefined, we want to make sure it is defined
|
|
app.subgrids[key] = []
|
|
}
|
|
|
|
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
|
|
|
const newItem = createNewGridItem(
|
|
grid,
|
|
id,
|
|
data,
|
|
columns,
|
|
initialPosition,
|
|
recOverride,
|
|
fixed,
|
|
shouldNotFindSpace
|
|
)
|
|
grid.push(newItem)
|
|
return id
|
|
}
|
|
|
|
export function copyComponent(
|
|
app: App,
|
|
item: GridItem,
|
|
parentGrid: FocusedGrid | undefined,
|
|
subgrids: Record<string, GridItem[]>,
|
|
alreadyVisited: string[]
|
|
) {
|
|
if (alreadyVisited.includes(item.id)) {
|
|
return
|
|
} else {
|
|
alreadyVisited.push(item.id)
|
|
}
|
|
const newItem = insertNewGridItem(
|
|
app,
|
|
(id) => {
|
|
if (item.data.type === 'tablecomponent') {
|
|
return {
|
|
...item.data,
|
|
id,
|
|
actionButtons:
|
|
item.data.actionButtons.map((x) => ({
|
|
...x,
|
|
id: x.id.replace(`${item.id}_`, `${id}_`)
|
|
})) ?? []
|
|
}
|
|
} else if (
|
|
item.data.type === 'aggridcomponent' ||
|
|
item.data.type === 'aggridcomponentee' ||
|
|
item.data.type === 'dbexplorercomponent' ||
|
|
item.data.type === 'aggridinfinitecomponent' ||
|
|
item.data.type === 'aggridinfinitecomponentee'
|
|
) {
|
|
return {
|
|
...item.data,
|
|
id,
|
|
actionButtons:
|
|
(item.data.actions ?? []).map((x) => ({
|
|
...x,
|
|
id: x.id.replace(`${item.id}_`, `${id}_`)
|
|
})) ?? []
|
|
}
|
|
} else if (item.data.type === 'menucomponent') {
|
|
return {
|
|
...item.data,
|
|
id,
|
|
menuItems:
|
|
item.data.menuItems.map((x) => ({
|
|
...x,
|
|
id: x.id.replace(`${item.id}_`, `${id}_`)
|
|
})) ?? []
|
|
}
|
|
} else {
|
|
return { ...item.data, id }
|
|
}
|
|
},
|
|
parentGrid,
|
|
Object.fromEntries(gridColumns.map((column) => [column, item[column]]))
|
|
)
|
|
|
|
for (let i = 0; i < (item?.data?.numberOfSubgrids ?? 0); i++) {
|
|
subgrids[`${item.id}-${i}`].forEach((subgridItem) => {
|
|
copyComponent(
|
|
app,
|
|
subgridItem,
|
|
{ parentComponentId: newItem, subGridIndex: i },
|
|
subgrids,
|
|
alreadyVisited
|
|
)
|
|
})
|
|
}
|
|
|
|
return newItem
|
|
}
|
|
|
|
export function getAllSubgridsAndComponentIds(
|
|
app: App,
|
|
component: AppComponent
|
|
): [string[], string[]] {
|
|
const subgrids: string[] = []
|
|
let components: string[] = [component.id]
|
|
if (component.type === 'tablecomponent') {
|
|
components.push(...component.actionButtons?.map((x) => x.id))
|
|
}
|
|
|
|
if (
|
|
component.type === 'aggridcomponent' ||
|
|
component.type === 'aggridcomponentee' ||
|
|
component.type === 'dbexplorercomponent' ||
|
|
component.type === 'aggridinfinitecomponent' ||
|
|
component.type === 'aggridinfinitecomponentee'
|
|
) {
|
|
components.push(...(component.actions?.map((x) => x.id) ?? []))
|
|
}
|
|
|
|
if (component.type === 'menucomponent') {
|
|
components.push(...component.menuItems?.map((x) => x.id))
|
|
}
|
|
|
|
if (app.subgrids && component.numberOfSubgrids) {
|
|
for (let i = 0; i < component.numberOfSubgrids; i++) {
|
|
const key = `${component.id}-${i}`
|
|
const subgrid = app.subgrids[key]
|
|
if (subgrid) {
|
|
subgrids.push(key)
|
|
for (const item of subgrid) {
|
|
let [recSubgrids, recComponents] = getAllSubgridsAndComponentIds(app, item.data)
|
|
subgrids.push(...recSubgrids)
|
|
components.push(...recComponents)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return [subgrids, components]
|
|
}
|
|
|
|
export function getAllGridItems(app: App): GridItem[] {
|
|
return app.grid
|
|
.concat(Object.values(app.subgrids ?? {}).flat())
|
|
.map((x) => {
|
|
if (x?.data?.type === 'tablecomponent') {
|
|
return [x, ...x?.data?.actionButtons?.map((x) => ({ data: x, id: x.id }))]
|
|
} else if (
|
|
(x?.data?.type === 'aggridcomponent' ||
|
|
x?.data?.type === 'aggridcomponentee' ||
|
|
x?.data?.type === 'dbexplorercomponent' ||
|
|
x?.data?.type === 'aggridinfinitecomponent' ||
|
|
x?.data?.type === 'aggridinfinitecomponentee') &&
|
|
Array.isArray(x?.data?.actions)
|
|
) {
|
|
return [x, ...x?.data?.actions?.map((x) => ({ data: x, id: x.id }))]
|
|
} else if (x?.data?.type === 'menucomponent') {
|
|
return [x, ...x?.data?.menuItems?.map((x) => ({ data: x, id: x.id }))]
|
|
}
|
|
return [x]
|
|
})
|
|
.flat()
|
|
}
|
|
|
|
export function deleteGridItem(
|
|
app: App,
|
|
component: AppComponent,
|
|
parent: string | undefined
|
|
): string[] {
|
|
let [subgrids, components] = getAllSubgridsAndComponentIds(app, component)
|
|
if (app.subgrids) {
|
|
subgrids.forEach((id) => {
|
|
delete app.subgrids![id]
|
|
})
|
|
}
|
|
if (parent) {
|
|
app.subgrids &&
|
|
(app.subgrids[parent] = app.subgrids[parent].filter((item) => item.id !== component?.id))
|
|
} else {
|
|
app.grid = app.grid.filter((item) => item.id !== component?.id)
|
|
}
|
|
return components
|
|
}
|
|
|
|
type AvailableSpace = {
|
|
left: number
|
|
right: number
|
|
top: number
|
|
bottom: number
|
|
}
|
|
|
|
export function findAvailableSpace(
|
|
grid: GridItem[],
|
|
gridItem: GridItem,
|
|
editorBreakpoint: EditorBreakpoint,
|
|
parentGridItem: GridItem | undefined = undefined
|
|
): AvailableSpace | undefined {
|
|
if (gridItem) {
|
|
const breakpoint = editorBreakpoint === 'sm' ? 3 : 12
|
|
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16
|
|
const maxWidth = 12
|
|
|
|
const availableSpace = {
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0
|
|
}
|
|
|
|
const items = grid.map((item) => {
|
|
return {
|
|
id: item.id,
|
|
x: item[breakpoint].x,
|
|
y: item[breakpoint].y,
|
|
w: item[breakpoint].w,
|
|
h: item[breakpoint].h
|
|
}
|
|
})
|
|
|
|
const item = items.find((item) => item.id === gridItem.id)
|
|
|
|
if (!item) {
|
|
return availableSpace
|
|
}
|
|
|
|
if (item.x > 0) {
|
|
for (let x = item.x - 1; x >= 0; x--) {
|
|
const itemToCheck = { ...item, x, w: 1 }
|
|
const isItemInWay = items.some((item) => isOverlapping(item, itemToCheck))
|
|
|
|
if (isItemInWay) {
|
|
break
|
|
} else {
|
|
availableSpace.left++
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.x + item.w < maxWidth) {
|
|
for (let x = item.x + item.w; x < maxWidth; x++) {
|
|
const itemToCheck = { ...item, x, w: 1 }
|
|
const isItemInWay = items.some((item) => isOverlapping(item, itemToCheck))
|
|
|
|
if (isItemInWay) {
|
|
break
|
|
} else {
|
|
availableSpace.right++
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.y > 0) {
|
|
for (let y = item.y - 1; y >= 0; y--) {
|
|
const itemToCheck = { ...item, h: 1, y }
|
|
const isItemInWay = items.some((item) => isOverlapping(item, itemToCheck))
|
|
|
|
if (isItemInWay) {
|
|
break
|
|
} else {
|
|
availableSpace.top++
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.y + item.h < maxHeight) {
|
|
for (let y = item.y + item.h; y < maxHeight; y++) {
|
|
const itemToCheck = { ...item, h: 1, y }
|
|
const isItemInWay = items.some((item) => isOverlapping(item, itemToCheck))
|
|
|
|
if (isItemInWay) {
|
|
break
|
|
} else {
|
|
availableSpace.bottom++
|
|
}
|
|
}
|
|
}
|
|
|
|
return availableSpace
|
|
}
|
|
}
|
|
|
|
function isOverlapping(item1: any, item2: any) {
|
|
return (
|
|
item1.x < item2.x + item2.w &&
|
|
item1.x + item1.w > item2.x &&
|
|
item1.y < item2.y + item2.h &&
|
|
item1.y + item1.h > item2.y
|
|
)
|
|
}
|
|
|
|
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> {
|
|
if (!world) {
|
|
return {} as any
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(init).map(([key, value]) => [key, world.newOutput(id, key, value)])
|
|
) as Outputtable<I>
|
|
}
|
|
|
|
export type InitConfig<
|
|
T extends Record<
|
|
string,
|
|
| StaticAppInput
|
|
| EvalAppInput
|
|
| EvalV2AppInput
|
|
| {
|
|
type: 'oneOf'
|
|
selected: string
|
|
configuration: Record<
|
|
string,
|
|
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
|
>
|
|
}
|
|
>
|
|
> = {
|
|
[Property in keyof T]: T[Property] extends StaticAppInput
|
|
? T[Property]['value'] | undefined
|
|
: T[Property] extends { type: 'oneOf' }
|
|
? {
|
|
type: 'oneOf'
|
|
selected: keyof T[Property]['configuration']
|
|
configuration: {
|
|
[Choice in keyof T[Property]['configuration']]: {
|
|
[IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput
|
|
? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand
|
|
? () => Promise<T[Property]['configuration'][Choice][IT]['value'] | undefined>
|
|
: T[Property]['configuration'][Choice][IT]['value'] | undefined
|
|
: undefined
|
|
}
|
|
}
|
|
}
|
|
: undefined
|
|
}
|
|
|
|
export function initConfig<
|
|
T extends Record<
|
|
string,
|
|
| StaticAppInput
|
|
| EvalAppInput
|
|
| EvalV2AppInput
|
|
| {
|
|
type: 'oneOf'
|
|
selected: string
|
|
configuration: Record<
|
|
string,
|
|
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
|
>
|
|
}
|
|
>
|
|
>(
|
|
r: T,
|
|
configuration?: Record<
|
|
string,
|
|
| StaticAppInput
|
|
| {
|
|
type: 'oneOf'
|
|
selected: string
|
|
configuration: Record<
|
|
string,
|
|
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput | boolean>
|
|
>
|
|
}
|
|
| any
|
|
>
|
|
): InitConfig<T> {
|
|
return JSON.parse(
|
|
JSON.stringify(
|
|
Object.fromEntries(
|
|
Object.entries(r).map(([key, value]) =>
|
|
value.type == 'static'
|
|
? [
|
|
key,
|
|
configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined
|
|
]
|
|
: value.type == 'oneOf'
|
|
? [
|
|
key,
|
|
{
|
|
selected: value.selected,
|
|
type: 'oneOf',
|
|
configuration: Object.fromEntries(
|
|
Object.entries(value.configuration).map(([choice, config]) => {
|
|
const conf = initConfig(config, configuration?.[key]?.configuration?.[choice])
|
|
Object.entries(config).forEach(([innerKey, innerValue]) => {
|
|
if (innerValue.type === 'static' && !(innerKey in conf)) {
|
|
conf[innerKey] = innerValue.value
|
|
}
|
|
})
|
|
return [choice, conf]
|
|
})
|
|
)
|
|
}
|
|
]
|
|
: [key, undefined]
|
|
)
|
|
) as any
|
|
)
|
|
)
|
|
}
|
|
|
|
export function expandGriditem(
|
|
grid: GridItem[],
|
|
id: string,
|
|
$breakpoint: EditorBreakpoint,
|
|
parentGridItem: GridItem | undefined = undefined
|
|
) {
|
|
const gridComponent = grid.find((item) => item.id === id)
|
|
if (!gridComponent) return
|
|
const availableSpace = findAvailableSpace(grid, gridComponent, $breakpoint, parentGridItem)
|
|
if (!availableSpace) {
|
|
return
|
|
}
|
|
|
|
const { left, right, top, bottom } = availableSpace
|
|
const width = $breakpoint === 'sm' ? 3 : 12
|
|
const item = gridComponent[width]
|
|
|
|
item.x = item.x - left
|
|
item.y = item.y - top
|
|
item.w = item.w + left + right
|
|
item.h = item.h + top + bottom
|
|
}
|
|
|
|
export function sortGridItemsPosition<T>(
|
|
gridItems: FilledItem<T>[],
|
|
width: number
|
|
): FilledItem<T>[] {
|
|
return gridItems.sort((a: FilledItem<T>, b: FilledItem<T>) => {
|
|
const aX = a[width].x
|
|
const aY = a[width].y
|
|
const bX = b[width].x
|
|
const bY = b[width].y
|
|
|
|
if (aY < bY) {
|
|
return -1
|
|
} else if (aY > bY) {
|
|
return 1
|
|
} else {
|
|
if (aX < bX) {
|
|
return -1
|
|
} else if (aX > bX) {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
export function connectInput(
|
|
connectingInput: ConnectingInput,
|
|
componentId: string,
|
|
path: string,
|
|
componentType?: TypedComponent['type']
|
|
): ConnectingInput {
|
|
if (connectingInput) {
|
|
if (connectingInput.onConnect) {
|
|
connectingInput.onConnect({ componentId, path })
|
|
sendUserToast(`Connected to ${componentId}.${path}`, false)
|
|
}
|
|
|
|
connectingInput = {
|
|
opened: false,
|
|
input: {
|
|
connection: {
|
|
componentId,
|
|
path,
|
|
componentType
|
|
},
|
|
type: 'connected'
|
|
},
|
|
hoveredComponent: undefined,
|
|
onConnect: undefined
|
|
}
|
|
}
|
|
|
|
return connectingInput
|
|
}
|
|
|
|
export function recursivelyFilterKeyInJSON(
|
|
json: object,
|
|
search: string,
|
|
extraSearch?: string | undefined
|
|
): object {
|
|
if (json === null || json === undefined || typeof json != 'object') {
|
|
return json
|
|
}
|
|
if (!search || search == '') {
|
|
return json
|
|
}
|
|
let filteredJSON = {}
|
|
Object.keys(json).forEach((key) => {
|
|
if (
|
|
key.toLowerCase().includes(search.toLowerCase()) ||
|
|
extraSearch?.toLowerCase().includes(search.toLowerCase()) ||
|
|
(typeof json[key] === 'string' && json[key].toLowerCase().includes(search.toLowerCase())) ||
|
|
(typeof json[key] === 'number' &&
|
|
json[key].toString().toLowerCase().includes(search.toLowerCase()))
|
|
) {
|
|
filteredJSON[key] = json[key]
|
|
} else if (typeof json[key] === 'object') {
|
|
const res = recursivelyFilterKeyInJSON(json[key], search, extraSearch)
|
|
|
|
if (Object.keys(res ?? {}).length !== 0) {
|
|
filteredJSON[key] = res
|
|
}
|
|
}
|
|
})
|
|
return filteredJSON
|
|
}
|
|
|
|
export function collectOneOfFields(fields: AppInputs, app: App): Record<string, any[]> {
|
|
return Object.fromEntries(
|
|
Object.entries(fields ?? {})
|
|
.filter(([k, v]) => v.type == 'evalv2')
|
|
.map(([k, v]) => {
|
|
let field = v as EvalV2AppInput
|
|
if (!field.connections || field.connections.length !== 1) {
|
|
return [k, undefined]
|
|
}
|
|
const c = field.connections[0]
|
|
|
|
const gridItem = findGridItem(app, c.componentId)
|
|
|
|
if (field.expr !== c.componentId + '.' + c.id) {
|
|
return [k, undefined]
|
|
}
|
|
|
|
if (gridItem) {
|
|
const c = gridItem.data as AppComponent
|
|
if (c) {
|
|
if (
|
|
c.type === 'resourceselectcomponent' ||
|
|
c.type === 'selectcomponent' ||
|
|
c.type === 'multiselectcomponent' ||
|
|
c.type === 'multiselectcomponentv2'
|
|
) {
|
|
if (
|
|
(c.type === 'selectcomponent' ||
|
|
c.type === 'multiselectcomponent' ||
|
|
c.type === 'multiselectcomponentv2') &&
|
|
c.configuration?.create?.type === 'static' &&
|
|
c.configuration?.create?.value === true
|
|
) {
|
|
return [k, undefined]
|
|
}
|
|
if (c.configuration?.items?.type === 'static') {
|
|
const items = c.configuration.items.value
|
|
if (items && Array.isArray(items)) {
|
|
if (c.type === 'multiselectcomponent' || c.type === 'multiselectcomponentv2') {
|
|
return [k, items]
|
|
} else {
|
|
const options = items
|
|
.filter(
|
|
(item) => item && typeof item === 'object' && 'value' in item && item.value
|
|
)
|
|
.map((item) => item.value)
|
|
|
|
return [k, options]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [k, undefined]
|
|
})
|
|
.filter(([k, v]) => v !== undefined)
|
|
)
|
|
}
|
|
|
|
export const ROW_HEIGHT = 36
|
|
export const ROW_GAP_Y = 2
|
|
export const ROW_GAP_X = 4
|
|
|
|
export function maxHeight(
|
|
grid: GridItem[],
|
|
windowHeight: number,
|
|
breakpoint: EditorBreakpoint = 'lg'
|
|
) {
|
|
const totalRowHeight = ROW_HEIGHT + ROW_GAP_Y
|
|
let maxRows = Math.floor((windowHeight - ROW_GAP_Y) / totalRowHeight)
|
|
|
|
if (!grid.length) {
|
|
return maxRows
|
|
}
|
|
|
|
const breakpointKey = breakpoint === 'sm' ? 3 : 12
|
|
const maxRowPerGrid = grid.reduce((max, item) => {
|
|
const y = item[breakpointKey].y
|
|
const h = item[breakpointKey].h
|
|
return Math.max(max, y + h)
|
|
}, 0)
|
|
|
|
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
|
|
}
|
|
|
|
export function setUpTopBarComponentContent(id: string, app: App) {
|
|
insertNewGridItem(
|
|
app,
|
|
appComponentFromType(
|
|
'textcomponent',
|
|
|
|
{
|
|
disableNoText: {
|
|
value: true,
|
|
type: 'static',
|
|
fieldType: 'boolean'
|
|
},
|
|
tooltip: {
|
|
type: 'evalv2',
|
|
|
|
fieldType: 'text',
|
|
expr: '`Author: ${ctx.author}`',
|
|
connections: [
|
|
{
|
|
componentId: 'ctx',
|
|
id: 'author'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
undefined,
|
|
{
|
|
customCss: {
|
|
text: {
|
|
class: 'text-xl font-semibold whitespace-nowrap truncate' as any,
|
|
style: ''
|
|
}
|
|
},
|
|
verticalAlignment: 'center',
|
|
componnetInput: {
|
|
type: 'templatev2',
|
|
fieldType: 'template',
|
|
eval: '${ctx.summary}',
|
|
connections: [
|
|
{
|
|
id: 'summary',
|
|
componentId: 'ctx'
|
|
}
|
|
] as InputConnectionEval[]
|
|
}
|
|
}
|
|
) as (id: string) => AppComponent,
|
|
{
|
|
parentComponentId: id,
|
|
subGridIndex: 0
|
|
},
|
|
undefined,
|
|
'title',
|
|
undefined,
|
|
{
|
|
3: {
|
|
w: 6,
|
|
h: 1
|
|
},
|
|
12: {
|
|
w: 6,
|
|
h: 1
|
|
}
|
|
}
|
|
)
|
|
|
|
insertNewGridItem(
|
|
app,
|
|
appComponentFromType('recomputeallcomponent', undefined, undefined, {
|
|
horizontalAlignment: 'right'
|
|
}) as (id: string) => AppComponent,
|
|
{
|
|
parentComponentId: id,
|
|
subGridIndex: 0
|
|
},
|
|
undefined,
|
|
'recomputeall',
|
|
undefined,
|
|
{
|
|
3: {
|
|
w: 3,
|
|
h: 1
|
|
},
|
|
12: {
|
|
w: 6,
|
|
h: 1
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
export function isContainer(type: string): boolean {
|
|
return (
|
|
type === 'containercomponent' ||
|
|
type === 'tabscomponent' ||
|
|
type === 'verticalsplitpanescomponent' ||
|
|
type === 'horizontalsplitpanescomponent' ||
|
|
type === 'steppercomponent' ||
|
|
type === 'listcomponent' ||
|
|
type === 'decisiontreecomponent'
|
|
)
|
|
}
|
|
|
|
export function subGridIndexKey(type: string | undefined, id: string, world: World): number {
|
|
switch (type) {
|
|
case 'containercomponent':
|
|
case 'verticalsplitpanescomponent':
|
|
case 'horizontalsplitpanescomponent':
|
|
case 'listcomponent':
|
|
return 0
|
|
case 'tabscomponent': {
|
|
return (world?.outputsById?.[id]?.selectedTabIndex?.peak() as number) ?? 0
|
|
}
|
|
case 'steppercomponent': {
|
|
return (world?.outputsById?.[id]?.currentStepIndex?.peak() as number) ?? 0
|
|
}
|
|
case 'decisiontreecomponent': {
|
|
return (world?.outputsById?.[id]?.currentNodeIndex?.peak() as number) ?? 0
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
export function computePosition(
|
|
clientX: number,
|
|
clientY: number,
|
|
xPerPx: number,
|
|
yPerPx: number,
|
|
overlapped?: string,
|
|
element?: HTMLElement
|
|
) {
|
|
const overlappedElement = overlapped
|
|
? document.getElementById(`component-${overlapped}`)
|
|
: document.getElementById('root-grid')
|
|
|
|
const xRelativeToElement = element ? clientX - element.getBoundingClientRect().left : 0
|
|
const yRelativeToElement = element ? clientY - element.getBoundingClientRect().top : 0
|
|
|
|
const xRelativeToOverlappedElement = overlappedElement
|
|
? clientX - overlappedElement.getBoundingClientRect().left - xRelativeToElement
|
|
: 0
|
|
const yRelativeToOverlappedElement = overlappedElement
|
|
? clientY - overlappedElement.getBoundingClientRect().top - yRelativeToElement
|
|
: 0
|
|
|
|
const gridX = Math.max(Math.round(xRelativeToOverlappedElement / xPerPx) ?? 0, 0)
|
|
const gridY = Math.max(Math.round(yRelativeToOverlappedElement / yPerPx) ?? 0, 0)
|
|
|
|
return {
|
|
x: gridX,
|
|
y: gridY
|
|
}
|
|
}
|
|
|
|
export function getDeltaYByComponent(type: string) {
|
|
switch (type) {
|
|
case 'steppercomponent': {
|
|
return '36px + 0.5rem'
|
|
}
|
|
case 'tabscomponent': {
|
|
return '32px'
|
|
}
|
|
default:
|
|
return '0px'
|
|
}
|
|
}
|
|
|
|
export function getDeltaXByComponent(type: string) {
|
|
switch (type) {
|
|
case 'steppercomponent': {
|
|
return '0.5rem'
|
|
}
|
|
case 'tabscomponent': {
|
|
return '0px'
|
|
}
|
|
default:
|
|
return '0px'
|
|
}
|
|
}
|
|
|
|
export type GridShadow = {
|
|
x: number
|
|
y: number
|
|
xPerPx: number
|
|
yPerPx: number
|
|
w: number
|
|
h: number
|
|
}
|
|
|
|
export function areShadowsTheSame(
|
|
shadow1: GridShadow | undefined,
|
|
shadow2: GridShadow | undefined
|
|
) {
|
|
if (!shadow1 || !shadow2) {
|
|
return false
|
|
}
|
|
|
|
return (
|
|
shadow1.x === shadow2.x &&
|
|
shadow1.y === shadow2.y &&
|
|
shadow1.xPerPx === shadow2.xPerPx &&
|
|
shadow1.yPerPx === shadow2.yPerPx &&
|
|
shadow1.w === shadow2.w &&
|
|
shadow1.h === shadow2.h
|
|
)
|
|
}
|