Recursive schema display and property deletion (#1539)
* Recursive schema display and property deletion * Added type safety and reintroduced lightmode --------- Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
This commit is contained in:
@@ -6,6 +6,15 @@ export type ActionKind = 'Create' | 'Update' | 'Delete' | 'Execute'
|
||||
|
||||
export type SupportedLanguage = Script.language
|
||||
|
||||
export interface PropertyDisplayInfo {
|
||||
property: SchemaProperty
|
||||
name: string
|
||||
isRequired: boolean
|
||||
path: string[]
|
||||
index: number
|
||||
propertiesNumber: number
|
||||
}
|
||||
|
||||
export interface SchemaProperty {
|
||||
type: string | undefined
|
||||
description?: string
|
||||
@@ -16,6 +25,7 @@ export interface SchemaProperty {
|
||||
format?: string
|
||||
items?: { type?: 'string' | 'number' | 'bytes' | 'object'; contentEncoding?: 'base64' }
|
||||
properties?: { [name: string]: SchemaProperty }
|
||||
required?: string[]
|
||||
}
|
||||
|
||||
export type Schema = {
|
||||
|
||||
90
frontend/src/lib/components/PropertyRow.svelte
Normal file
90
frontend/src/lib/components/PropertyRow.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import Required from './Required.svelte'
|
||||
import type { PropertyDisplayInfo } from '$lib/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { faPen, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import SchemaEditorProperty from './SchemaEditorProperty.svelte'
|
||||
import { Button } from './common'
|
||||
|
||||
export let displayInfo: PropertyDisplayInfo
|
||||
export let isAnimated: boolean
|
||||
export let lightMode: boolean
|
||||
|
||||
let depth = displayInfo.path.length
|
||||
const required = displayInfo.isRequired
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function startEditArgument(name: string) {
|
||||
dispatch('startEditArgument', name)
|
||||
}
|
||||
|
||||
function handleDeleteArgument(argPath: string[]) {
|
||||
dispatch('deleteArgument', argPath)
|
||||
}
|
||||
|
||||
function changePosition(i: number, up: boolean) {
|
||||
dispatch('changePosition', { i, up })
|
||||
}
|
||||
|
||||
function getArgPath(displayInfo: PropertyDisplayInfo): string[] {
|
||||
return [...displayInfo.path, displayInfo.name]
|
||||
}
|
||||
|
||||
const indentStyle = depth > 0 ? `margin-left :${depth}rem` : ''
|
||||
</script>
|
||||
|
||||
<td class="font-bold">
|
||||
<span style={indentStyle}>
|
||||
{displayInfo.name}
|
||||
<Required {required} class="!ml-0" />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<SchemaEditorProperty property={displayInfo.property} />
|
||||
</td>
|
||||
{#if !lightMode}
|
||||
<td>{displayInfo.property.default ? JSON.stringify(displayInfo.property.default) : ''}</td>
|
||||
<td>{displayInfo.property.description ?? ''}</td>
|
||||
{/if}
|
||||
<td />
|
||||
<td class="justify-end flex">
|
||||
{#if depth === 0}
|
||||
{#if displayInfo.index > 0}
|
||||
<button
|
||||
on:click={() => changePosition(displayInfo.index, true)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>
|
||||
↑</button
|
||||
>
|
||||
{/if}
|
||||
{#if displayInfo.index < displayInfo.propertiesNumber - 1}
|
||||
<button
|
||||
on:click={() => changePosition(displayInfo.index, false)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">↓</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faPen }}
|
||||
on:click={() => startEditArgument(displayInfo.name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
color="red"
|
||||
variant="border"
|
||||
btnClasses="mx-2"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => handleDeleteArgument(getArgPath(displayInfo))}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Schema, SchemaProperty } from '$lib/common'
|
||||
import type { Schema, SchemaProperty, PropertyDisplayInfo } from '$lib/common'
|
||||
import { emptySchema, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { faPen, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from './common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SchemaEditorProperty from './SchemaEditorProperty.svelte'
|
||||
import type { ModalSchemaProperty } from './SchemaModal.svelte'
|
||||
import SchemaModal, { DEFAULT_PROPERTY, schemaToModal } from './SchemaModal.svelte'
|
||||
import PropertyRow from './PropertyRow.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
@@ -19,6 +19,9 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let lightMode: boolean = false
|
||||
|
||||
const moveAnimationDuration = 300
|
||||
|
||||
export let schema: Schema = emptySchema()
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
@@ -83,7 +86,7 @@
|
||||
}
|
||||
|
||||
if (editing && oldArgName && oldArgName !== modalProperty.name) {
|
||||
handleDeleteArgument(oldArgName)
|
||||
handleDeleteArgument([oldArgName])
|
||||
}
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
editing = false
|
||||
@@ -96,6 +99,10 @@
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
function handleStartEditEvent(event: CustomEvent): void {
|
||||
startEditArgument(event.detail)
|
||||
}
|
||||
|
||||
function startEditArgument(argName: string): void {
|
||||
argError = ''
|
||||
if (Object.keys(schema.properties).includes(argName)) {
|
||||
@@ -112,12 +119,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteArgument(argName: string): void {
|
||||
try {
|
||||
if (Object.keys(schema.properties).includes(argName)) {
|
||||
delete schema.properties[argName]
|
||||
function handleDeleteEvent(event: CustomEvent): void {
|
||||
handleDeleteArgument(event.detail)
|
||||
}
|
||||
|
||||
schema.required = schema.required.filter((arg) => arg !== argName)
|
||||
function handleDeleteArgument(argPath: string[]): void {
|
||||
try {
|
||||
let modifiedObject: Schema | SchemaProperty = schema
|
||||
let modifiedProperties = modifiedObject.properties as object
|
||||
let argName = argPath.pop() as string
|
||||
|
||||
argPath.forEach((property) => {
|
||||
if (Object.keys(modifiedProperties).includes(property)) {
|
||||
modifiedObject = modifiedProperties[property]
|
||||
modifiedProperties = modifiedObject.properties as object
|
||||
} else {
|
||||
throw Error('Nested argument not found!')
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
@@ -142,11 +166,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePositionEvent(event: CustomEvent): void {
|
||||
changePosition(event.detail.i, event.detail.up)
|
||||
}
|
||||
|
||||
function changePosition(i: number, up: boolean): any {
|
||||
isAnimated = true
|
||||
setTimeout(() => {
|
||||
isAnimated = false
|
||||
}, 500)
|
||||
}, moveAnimationDuration)
|
||||
const entries = Object.entries(schema.properties)
|
||||
var element = entries[i]
|
||||
entries.splice(i, 1)
|
||||
@@ -155,6 +183,45 @@
|
||||
}
|
||||
let isAnimated = false
|
||||
let error = ''
|
||||
|
||||
function schemaPropertiesToDisplay(schema: Schema): PropertyDisplayInfo[] {
|
||||
return propertiesToDisplay(schema.properties, schema.required, [])
|
||||
}
|
||||
|
||||
function propertiesToDisplay(
|
||||
properties: { [name: string]: SchemaProperty },
|
||||
required: string[],
|
||||
path: string[]
|
||||
): PropertyDisplayInfo[] {
|
||||
return Object.entries(properties)
|
||||
.map(([name, property], index) => {
|
||||
const isRequired = required.includes(name)
|
||||
const displayInfo = {
|
||||
property: property,
|
||||
name: name,
|
||||
isRequired: isRequired,
|
||||
path: path,
|
||||
index: index,
|
||||
propertiesNumber: Object.entries(properties).length
|
||||
}
|
||||
if (property.type === 'object') {
|
||||
const newPath = [...path, name]
|
||||
return [
|
||||
displayInfo,
|
||||
...propertiesToDisplay(property.properties || {}, property.required || [], newPath)
|
||||
]
|
||||
} else {
|
||||
return [displayInfo]
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
/* Small hash function to generate a unique key for each property */
|
||||
function displayInfoKey(displayInfo: PropertyDisplayInfo): string {
|
||||
const pathLengthString = displayInfo.path.length.toString()
|
||||
return pathLengthString + [...displayInfo.path, displayInfo.name].join(pathLengthString)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
@@ -163,7 +230,7 @@
|
||||
<Button
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
@@ -180,7 +247,6 @@
|
||||
options={{
|
||||
right: 'As JSON'
|
||||
}}
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<Tooltip
|
||||
@@ -202,67 +268,22 @@
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
{#if !lightMode}
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
{/if}
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each Object.entries(schema.properties) as [name, property], i (name)}
|
||||
<tr animate:flip>
|
||||
<td class="font-bold">{name}</td>
|
||||
<td>
|
||||
<SchemaEditorProperty {property} />
|
||||
</td>
|
||||
{#if !lightMode}
|
||||
<td>{property.description ?? ''}</td>
|
||||
<td>{property.default ? JSON.stringify(property.default) : ''}</td>
|
||||
<td>
|
||||
{#if schema.required.includes(name)}
|
||||
<span class="text-red-600 font-bold text-lg">*</span>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td class="justify-end flex">
|
||||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => changePosition(i, true)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
{/if}
|
||||
{#if i < Object.keys(schema.properties).length - 1}
|
||||
<button
|
||||
on:click={() => changePosition(i, false)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>↓
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
variant="border"
|
||||
btnClasses="mx-2"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => handleDeleteArgument(name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faPen }}
|
||||
on:click={() => startEditArgument(name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
{#each schemaPropertiesToDisplay(schema) as displayInfo (displayInfoKey(displayInfo))}
|
||||
<tr animate:flip={{ duration: moveAnimationDuration }}>
|
||||
<PropertyRow
|
||||
{displayInfo}
|
||||
{isAnimated}
|
||||
{lightMode}
|
||||
on:startEditArgument={handleStartEditEvent}
|
||||
on:deleteArgument={handleDeleteEvent}
|
||||
on:changePosition={handleChangePositionEvent}
|
||||
/>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user