Thomas G. Lopes
Adjustments (#86)
d7cd63b unverified
raw
history blame
8.59 kB
<script lang="ts" module>
const propertyTypes = ["string", "integer", "number", "boolean", "object", "enum", "array"] as const;
export type PropertyType = (typeof propertyTypes)[number];
export type PropertyDefinition = {
type: PropertyType;
description?: string;
enum?: string[];
properties?: { [key: string]: PropertyDefinition };
items?: PropertyDefinition;
};
// Example:
// {
// "type": "object",
// "properties": {
// "static": {
// "type": "string"
// },
// "array": {
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "enum": {
// "type": "string",
// "enum": [
// "V1",
// "V2",
// "V3"
// ]
// },
// "object": {
// "type": "object",
// "properties": {
// "key1": {
// "type": "string"
// },
// "key2": {
// "type": "string"
// }
// }
// }
// }
// }
</script>
<script lang="ts">
import { onchange } from "$lib/utils/template.js";
import IconX from "~icons/carbon/close";
import IconAdd from "~icons/carbon/add-large";
import SchemaProperty from "./schema-property.svelte";
import Tooltip from "../tooltip.svelte";
type Props = {
name: string;
definition: PropertyDefinition;
required?: boolean;
nesting?: number;
onDelete: () => void;
};
let { name = $bindable(), definition = $bindable(), onDelete, required = $bindable(), nesting = 0 }: Props = $props();
// If isArray, this will be the inner type of the array. Otherwise it will be the definition itself.
const innerDefinition = {
get $() {
if (definition.type === "array") {
return definition.items ?? { type: "string" };
}
return definition;
},
set $(v) {
if (isArray.current) {
definition = { ...definition, items: v };
return;
}
definition = v;
},
};
const type = {
get $() {
return "enum" in innerDefinition.$ ? "enum" : innerDefinition.$.type;
},
set $(v) {
delete definition.enum;
delete definition.properties;
const inner = { type: v === "enum" ? "string" : v } as PropertyDefinition;
if (v === "enum") inner.enum = [];
if (definition.type === "array") {
definition = { ...definition, items: inner };
}
definition = { ...definition, ...inner };
},
};
const isArray = {
get current() {
return definition.type === "array";
},
set current(v) {
delete definition.enum;
delete definition.properties;
if (v) {
definition = { ...definition, type: "array", items: { type: definition.type } };
} else {
const prevType = definition.items?.type ?? "string";
delete definition.items;
definition = { ...definition, type: prevType };
}
},
};
const nestingClasses = [
"border-gray-300 dark:border-gray-700",
"border-gray-300 dark:border-gray-600",
"border-gray-300 dark:border-gray-500",
"border-gray-300 dark:border-gray-600",
];
</script>
<div
class={[
"relative space-y-2 border-l-2 bg-white py-2 pl-4 dark:bg-gray-900",
...nestingClasses.map((c, i) => {
return nesting % nestingClasses.length === i && c;
}),
]}
>
<div class="flex gap-2">
<div class="grow">
<label for="{name}-name" class="block text-xs font-medium text-gray-500 dark:text-gray-400"> Name </label>
<input
type="text"
id="{name}-name"
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
value={name}
{...onchange(v => (name = v))}
/>
</div>
<div class="grow">
<label for="{name}-type" class="block text-xs font-medium text-gray-500 dark:text-gray-400"> Type </label>
<select
id="{name}-type"
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-2 py-1.25 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
bind:value={() => type.$, v => (type.$ = v)}
>
{#each propertyTypes.filter(t => t !== "array") as type}
<option value={type}>{type}</option>
{/each}
</select>
</div>
{#if type.$ === "object"}
<Tooltip>
{#snippet trigger(tooltip)}
<button
type="button"
class="btn-xs self-end rounded-md"
onclick={() => {
const prevProperties = innerDefinition.$.properties || {};
innerDefinition.$ = { ...innerDefinition.$, properties: { ...prevProperties, "": { type: "string" } } };
}}
aria-label="Add nested"
{...tooltip.trigger}
>
<IconAdd />
</button>
{/snippet}
Add nested property
</Tooltip>
{/if}
{#if type.$ === "enum"}
<Tooltip>
{#snippet trigger(tooltip)}
<button
type="button"
class="btn-xs self-end rounded-md"
onclick={() => {
const prevValues = innerDefinition.$.enum || [];
innerDefinition.$ = { ...innerDefinition.$, enum: [...prevValues, ""] };
}}
aria-label="Add enum value"
{...tooltip.trigger}
>
<IconAdd />
</button>
{/snippet}
Add enum value
</Tooltip>
{/if}
<button
type="button"
class="btn-xs self-end rounded-md text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-500"
onclick={onDelete}
aria-label="delete"
>
<IconX />
</button>
</div>
{#if !nesting}
<div class="flex items-start">
<div class="flex h-5 items-center">
<input
id="required-{name}"
name="required-{name}"
type="checkbox"
class="h-4 w-4 rounded border border-gray-300 bg-white text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
bind:checked={required}
/>
</div>
<div class="ml-3 text-sm">
<label for="required-{name}" class="font-medium text-gray-700 dark:text-gray-300"> Required </label>
</div>
</div>
{/if}
<div class="flex items-start">
<div class="flex h-5 items-center">
<input
id="is-array-{name}"
name="is-array-{name}"
type="checkbox"
class="h-4 w-4 rounded border border-gray-300 bg-white text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
bind:checked={isArray.current}
/>
</div>
<div class="ml-3 text-sm">
<label for="is-array-{name}" class="font-medium text-gray-700 dark:text-gray-300"> Array </label>
</div>
</div>
{#if type.$ === "object"}
{#each Object.entries(innerDefinition.$.properties ?? {}) as [propertyName, propertyDefinition], index (index)}
<SchemaProperty
bind:name={
() => propertyName,
value => {
const nd = { ...innerDefinition.$ };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nd.properties![value] = innerDefinition.$.properties![propertyName] as any;
delete nd.properties![propertyName];
innerDefinition.$ = nd;
}
}
bind:definition={
() => propertyDefinition,
v => {
innerDefinition.$.properties![propertyName] = v;
innerDefinition.$ = innerDefinition.$;
}
}
onDelete={() => {
delete innerDefinition.$.properties![propertyName];
innerDefinition.$ = innerDefinition.$;
}}
nesting={nesting + 1}
/>
{/each}
{/if}
{#if "enum" in innerDefinition.$}
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Values</p>
{#each innerDefinition.$.enum ?? [] as val, index (index)}
<div
class={[
"flex border-l-2 pl-2",
...nestingClasses.map((c, i) => {
return (nesting + 1) % nestingClasses.length === i && c;
}),
]}
>
<input
id="{name}-enum-{index}"
class="block w-full rounded-md border border-gray-300 bg-white px-2 py-1
text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
type="text"
value={val}
{...onchange(v => {
innerDefinition.$.enum = innerDefinition.$.enum ?? [];
innerDefinition.$.enum[index] = v;
innerDefinition.$ = innerDefinition.$;
})}
/>
<button
type="button"
class="btn-xs ml-2 rounded-md text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-500"
onclick={() => {
innerDefinition.$.enum = innerDefinition.$.enum ?? [];
innerDefinition.$.enum.splice(index, 1);
innerDefinition.$ = innerDefinition.$;
}}
>
<IconX />
</button>
</div>
{:else}
<p class="mt-2 text-xs italic text-gray-400">No enum values defined.</p>
{/each}
{/if}
</div>