Skip to content

2536 - Improve UI of JSONSchemaBuilder #2574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 16, 2025
2 changes: 2 additions & 0 deletions client/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"bg-surface-neutral-secondary-hover",
"bg-surface-neutral-tertiary",
"bg-surface-popover-canvas",
"bg-surface-warning-primary",
"bg-surface-warning-secondary",
"border-accent",
"border-content-neutral-tertiary",
"border-input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const JsonSchemaBuilder = ({locale = 'en', onChange, schema}: JsonSchemaBuilderP
...schema,
});
}}
root
schema={curSchema}
/>
);
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

223 changes: 152 additions & 71 deletions client/src/components/JsonSchemaBuilder/components/SchemaControls.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,193 @@
import SchemaAddButton from '@/components/JsonSchemaBuilder/components/SchemaAddButton';
import SchemaCollapseButton from '@/components/JsonSchemaBuilder/components/SchemaCollapseButton';
import SchemaDeleteButton from '@/components/JsonSchemaBuilder/components/SchemaDeleteButton';
import SchemaInput from '@/components/JsonSchemaBuilder/components/SchemaInput';
import SchemaMenu from '@/components/JsonSchemaBuilder/components/SchemaMenu';
import SchemaMenuButton from '@/components/JsonSchemaBuilder/components/SchemaMenuButton';
import SchemaMenuModal from '@/components/JsonSchemaBuilder/components/SchemaMenuModal';
import SchemaMenuPopover from '@/components/JsonSchemaBuilder/components/SchemaMenuPopover';
import SchemaTypesSelect from '@/components/JsonSchemaBuilder/components/SchemaTypesSelect';
import React, {useState} from 'react';
import {useTranslation} from 'react-i18next';

import * as helpers from '../utils/helpers';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {CircleEllipsisIcon, PlusIcon, TrashIcon} from 'lucide-react';
import {useEffect, useState} from 'react';
import {twMerge} from 'tailwind-merge';

import {
addSchemaProperty,
getSchemaTitle,
getSchemaType,
setSchemaTitle,
setSchemaTypeAndRemoveWrongFields,
} from '../utils/helpers';
import {SchemaRecordType} from '../utils/types';

interface SchemaControlsProps {
schema: SchemaRecordType;
schemakey: string;
isCollapsed?: boolean;
onDelete?: () => void;
onAdd?: () => void;
onCollapse?: () => void;
onChangeKey?: (key: string) => void;
onChange: (schema: SchemaRecordType) => void;
onCollapse?: () => void;
onDelete?: () => void;
root?: boolean;
schema: SchemaRecordType;
schemakey: string;
}

export const SchemaControls = ({
isCollapsed,
onAdd,
onChange,
onChangeKey,
onCollapse,
onDelete,
schema,
schemakey,
}: SchemaControlsProps) => {
const SchemaControls = ({onAdd, onChange, onChangeKey, onDelete, root, schema, schemakey}: SchemaControlsProps) => {
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);

const {t} = useTranslation();
const isObjectSchema = getSchemaType(schema) === 'object';

const extraFields = Object.keys(schema).filter((key) => key !== 'type' && key !== 'items' && key !== 'properties');

useEffect(() => {
if (!schema.type || !getSchemaType(schema)) {
onChange(setSchemaTypeAndRemoveWrongFields('object', schema));
}
}, [schema, onChange]);

return (
<div className="flex flex-row items-end">
<div className="mr-2 grid grid-flow-col gap-2">
<SchemaInput
label={t('title')}
onChange={(title) => onChange(helpers.setSchemaTitle(title, schema))}
placeholder={t('title')}
value={helpers.getSchemaTitle(schema)}
<div className="flex w-full items-end">
<div className={twMerge('flex gap-2', root ? 'mr-1' : 'flex-1')}>
<SchemaTypesSelect
onChange={(translation) => onChange(setSchemaTypeAndRemoveWrongFields(translation, schema))}
type={getSchemaType(schema)}
/>

<SchemaTypesSelect
onChange={(t) => onChange(helpers.setSchemaTypeAndRemoveWrongFields(t, schema))}
type={helpers.getSchemaType(schema)}
<SchemaInput
label="Pill Title"
onChange={(title) => onChange(setSchemaTitle(title, schema))}
placeholder="Untitled Pill"
value={getSchemaTitle(schema)}
/>

{typeof onChangeKey === 'function' ? (
<SchemaInput label={t('key')} onChange={onChangeKey} placeholder={t('key')} value={schemakey} />
) : null}
{typeof onChangeKey === 'function' && (
<SchemaInput label="Pill Key" onChange={onChangeKey} placeholder="Pill Key" value={schemakey} />
)}
</div>

<div className="mb-0.5 grid grid-flow-col items-center gap-1">
{typeof onCollapse === 'function' ? (
<SchemaCollapseButton isCollapsed={isCollapsed} onClick={onCollapse} title={t('collapse')} />
) : null}

<SchemaMenuButton onClick={() => setIsMenuOpen((o) => !o)} title={t('extraOptions')} />

{typeof onDelete === 'function' ? <SchemaDeleteButton onClick={onDelete} title={t('delete')} /> : null}

{typeof onAdd === 'function' ? <SchemaAddButton onClick={onAdd} title={t('add')} /> : null}
<div
className={twMerge(
'ml-auto grid shrink-0 grid-flow-col items-center gap-1',
root && 'ml-0 flex-1 justify-between'
)}
>
<SchemaMenuPopover
onChange={onChange}
onClose={() => setIsMenuOpen(false)}
open={isMenuOpen}
schema={schema}
>
<Button
className={twMerge('group px-2.5', isMenuOpen && 'bg-surface-brand-secondary')}
onClick={() => setIsMenuOpen((open) => !open)}
title="Extra fields"
variant="ghost"
>
<CircleEllipsisIcon className={twMerge(isMenuOpen && 'text-content-brand-primary')} />

{root && <span>Extra fields</span>}

{extraFields.length > 0 && (
<Badge
className={twMerge(
'group-hover:bg-surface-neutral-secondary-hover',
isMenuOpen && 'bg-surface-brand-primary text-white'
)}
variant="secondary"
>
{extraFields.length}
</Badge>
)}
</Button>
</SchemaMenuPopover>

{typeof onDelete === 'function' && (
<Button
className="text-content-destructive/50 hover:bg-surface-destructive-secondary hover:text-content-destructive"
onClick={onDelete}
size="icon"
title="Delete"
variant="ghost"
>
<TrashIcon />
</Button>
)}

{(typeof onAdd === 'function' || isObjectSchema) && (
<Button
className={twMerge(
root && 'ml-auto bg-surface-brand-primary hover:bg-surface-brand-primary-hover'
)}
disabled={!isObjectSchema}
onClick={onAdd || (() => onChange(addSchemaProperty(schema)))}
size={root ? 'default' : 'icon'}
title="Add Property"
variant={root ? 'default' : 'ghost'}
>
<PlusIcon />

{root && <span>Add a pill</span>}
</Button>
)}
</div>

{isMenuOpen && (
<SchemaMenuModal onClose={() => setIsMenuOpen(false)} title={t('extraFields')}>
<SchemaMenu onChange={onChange} schema={schema} />
</SchemaMenuModal>
)}
</div>
);
};

interface SchemaArrayControlsProps {
schema: SchemaRecordType;
onChange: (schema: SchemaRecordType) => void;
onAdd?: () => void;
onChange: (schema: SchemaRecordType) => void;
root?: boolean;
schema: SchemaRecordType;
}

export const SchemaArrayControls = ({onAdd, onChange, schema}: SchemaArrayControlsProps) => {
const SchemaArrayControls = ({onAdd, onChange, root, schema}: SchemaArrayControlsProps) => {
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);

const {t} = useTranslation();
const extraFields = Object.keys(schema).filter((key) => key !== 'type' && key !== 'items' && key !== 'properties');

return (
<div className="flex items-end">
<div className="flex w-full items-center">
<SchemaTypesSelect
onChange={(t) => onChange(helpers.setSchemaTypeAndRemoveWrongFields(t, schema))}
type={helpers.getSchemaType(schema)}
onChange={(value) => onChange(setSchemaTypeAndRemoveWrongFields(value, schema))}
type={getSchemaType(schema)}
/>

<div className="mb-0.5 ml-2 grid grid-flow-col gap-1">
<SchemaMenuButton onClick={() => setIsMenuOpen((o) => !o)} title={t('extraOptions')} />

{typeof onAdd === 'function' ? <SchemaAddButton onClick={onAdd} title={t('add')} /> : null}
<div className="ml-auto flex space-x-1">
<SchemaMenuPopover
onChange={onChange}
onClose={() => setIsMenuOpen(false)}
open={isMenuOpen}
schema={schema}
>
<Button
className={twMerge('group px-2.5', isMenuOpen && 'bg-surface-brand-secondary')}
onClick={() => setIsMenuOpen((open) => !open)}
title="Extra fields"
variant="ghost"
>
<CircleEllipsisIcon className={twMerge(isMenuOpen && 'text-content-brand-primary')} />

{root && <span>Extra fields</span>}

{extraFields.length > 0 && (
<Badge
className={twMerge(
'group-hover:bg-surface-neutral-secondary-hover',
isMenuOpen && 'bg-surface-brand-primary text-white'
)}
variant="secondary"
>
{extraFields.length}
</Badge>
)}
</Button>
</SchemaMenuPopover>

{typeof onAdd === 'function' && (
<Button onClick={onAdd} size="icon" title="Add" variant="ghost">
<PlusIcon />
</Button>
)}
</div>

{isMenuOpen ? (
<SchemaMenuModal onClose={() => setIsMenuOpen(false)} title={t('extraFields')}>
<SchemaMenu onChange={onChange} schema={schema} />
</SchemaMenuModal>
) : null}
</div>
);
};

export {SchemaControls, SchemaArrayControls};
Loading
Loading