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:
Axel Baudot
2023-05-18 07:52:06 -04:00
committed by GitHub
parent 89e55e0226
commit d2c2e67718
3 changed files with 189 additions and 68 deletions

View File

@@ -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 = {

View 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' : ''}"
>
&uparrow;</button
>
{/if}
{#if displayInfo.index < displayInfo.propertiesNumber - 1}
<button
on:click={() => changePosition(displayInfo.index, false)}
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">&downarrow;</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>

View File

@@ -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' : ''}"
>
&uparrow;
</button>
{/if}
{#if i < Object.keys(schema.properties).length - 1}
<button
on:click={() => changePosition(i, false)}
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
>&downarrow;
</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>