import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react'; import React from 'react'; import { canonicalJsonString, jsonValuesEqual } from '../../../domain/convergentSync/json'; import type { JsonValue } from '../../../domain/convergentSync/types'; import { Button } from '../../ui/button'; import { Input } from '../../ui/input'; type RestrictedSchema = { type?: string; const?: unknown; enum?: unknown[]; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; minItems?: number; maxItems?: number; items?: RestrictedSchema; properties?: Record; required?: string[]; }; function asSchema(value: unknown): RestrictedSchema { return value && typeof value === 'object' && !Array.isArray(value) ? value as RestrictedSchema : {}; } export function createPluginStructuredDefaultValue(schema: RestrictedSchema): unknown { if (schema.const !== undefined) return structuredClone(schema.const); if (schema.enum?.length) return structuredClone(schema.enum[0]); if (schema.type === 'boolean') return false; if (schema.type === 'number' || schema.type === 'integer') { if (schema.type === 'integer') { return schema.minimum !== undefined ? Math.ceil(schema.minimum) : Math.min(0, Math.floor(schema.maximum ?? 0)); } return schema.minimum ?? Math.min(0, schema.maximum ?? 0); } if (schema.type === 'array') { return Array.from( { length: schema.minItems ?? 0 }, () => createPluginStructuredDefaultValue(asSchema(schema.items)), ); } if (schema.type === 'null') return null; if (schema.type === 'object') { const required = new Set(schema.required ?? []); return Object.fromEntries(Object.entries(schema.properties ?? {}) .filter(([key]) => required.has(key)) .map(([key, child]) => [key, createPluginStructuredDefaultValue(child)])); } return ''.padEnd(schema.minLength ?? 0, 'x'); } function StructuredValueInput({ schema, value, disabled, label, onChange, onCommit, sortable = false, labels, }: { schema: RestrictedSchema; value: unknown; disabled: boolean; label: string; onChange(value: unknown): void; onCommit(value: unknown): void; sortable?: boolean; labels: { add: string; remove: string; moveUp(index: number): string; moveDown(index: number): string }; }) { if (schema.const !== undefined) { return {JSON.stringify(schema.const)}; } if (schema.enum?.length) { const selectedIndex = schema.enum.findIndex((candidate) => ( jsonValuesEqual(candidate as JsonValue, value as JsonValue) )); return ( ); } if (schema.type === 'null') { return null; } if (schema.type === 'boolean') { return ( { onChange(event.target.checked); onCommit(event.target.checked); }} className="h-4 w-4 accent-primary" /> ); } if (schema.type === 'object') { const record = value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; const required = new Set(schema.required ?? []); return (
{Object.entries(schema.properties ?? {}).map(([key, child]) => { const present = Object.hasOwn(record, key); if (!required.has(key) && !present) { return (
{key}
); } return (
{key}
onChange({ ...record, [key]: next })} onCommit={(next) => onCommit({ ...record, [key]: next })} />
{!required.has(key) && }
); })}
); } if (schema.type === 'array') { const items = Array.isArray(value) ? value : []; const itemSchema = asSchema(schema.items); const commitItems = (next: unknown[]) => { onChange(next); onCommit(next); }; return (
{items.map((item, index) => (
{ const next = [...items]; next[index] = nextItem; onChange(next); }} onCommit={(nextItem) => { const next = [...items]; next[index] = nextItem; onCommit(next); }} />
{sortable && } {sortable && }
))}
); } const numeric = schema.type === 'number' || schema.type === 'integer'; return ( onChange(numeric ? Number(event.target.value) : event.target.value)} onBlur={(event) => onCommit(numeric ? Number(event.currentTarget.value) : event.currentTarget.value)} className="min-w-28" /> ); } export function PluginStructuredSettingEditor({ setting, value, disabled, onChange, onCommit, labels, }: { setting: NetcattyPluginSettingContribution; value: unknown; disabled: boolean; onChange(value: unknown[]): void; onCommit(value: unknown[]): void; labels: { add: string; remove: string; moveUp(index: number): string; moveDown(index: number): string }; }) { const root = asSchema(setting.valueSchema); const itemSchema = asSchema(root.items); const items = Array.isArray(value) ? value : []; const properties = itemSchema.type === 'object' && !itemSchema.enum?.length && itemSchema.const === undefined ? Object.entries(itemSchema.properties ?? {}) : []; const updateItem = (index: number, nextItem: unknown, commit: boolean) => { const next = [...items]; next[index] = nextItem; onChange(next); if (commit) onCommit(next); }; const move = (from: number, to: number) => { if (to < 0 || to >= items.length) return; const next = [...items]; const [item] = next.splice(from, 1); next.splice(to, 0, item); onChange(next); onCommit(next); }; const remove = (index: number) => { const next = items.filter((_item, itemIndex) => itemIndex !== index); onChange(next); onCommit(next); }; const add = () => { const next = [...items, createPluginStructuredDefaultValue(itemSchema)]; onChange(next); onCommit(next); }; return (
{properties.length > 0 && (
{properties.map(([key]) => {key})} Actions
)} {items.map((item, index) => { const objectItem = item && typeof item === 'object' && !Array.isArray(item) ? item as Record : {}; const required = new Set(itemSchema.required ?? []); return (
{properties.length ? properties.map(([key, schema]) => { const present = Object.hasOwn(objectItem, key); if (!required.has(key) && !present) { return ; } return (
updateItem(index, { ...objectItem, [key]: next }, false)} onCommit={(next) => updateItem(index, { ...objectItem, [key]: next }, true)} />
{!required.has(key) && }
); }) : ( updateItem(index, next, false)} onCommit={(next) => updateItem(index, next, true)} /> )}
{setting.sortable && ( <> )}
); })}
); }