mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(web): edit entity details from page titles
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
export interface EntityBasicInfoValues {
|
||||
name: string;
|
||||
description: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
interface EntityBasicInfoDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
values: EntityBasicInfoValues;
|
||||
defaultEmoji?: string;
|
||||
showEmoji?: boolean;
|
||||
onSave: (values: EntityBasicInfoValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function EntityBasicInfoDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
values,
|
||||
defaultEmoji,
|
||||
showEmoji = true,
|
||||
onSave,
|
||||
}: EntityBasicInfoDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [draft, setDraft] = useState(values);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [nameError, setNameError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setDraft({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
emoji: values.emoji || defaultEmoji,
|
||||
});
|
||||
setNameError(false);
|
||||
}, [defaultEmoji, open, values.description, values.emoji, values.name]);
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const name = draft.name.trim();
|
||||
if (!name) {
|
||||
setNameError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave({
|
||||
name,
|
||||
description: draft.description.trim(),
|
||||
emoji: showEmoji ? draft.emoji || defaultEmoji : undefined,
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
// The caller presents the entity-specific error message.
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.editBasicInfo')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t(
|
||||
showEmoji
|
||||
? 'common.editBasicInfoDescription'
|
||||
: 'common.editBasicInfoDescriptionNoIcon',
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<Label htmlFor="entity-basic-name">{t('common.name')}</Label>
|
||||
<Input
|
||||
id="entity-basic-name"
|
||||
value={draft.name}
|
||||
aria-invalid={nameError}
|
||||
onChange={(event) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
name: event.target.value,
|
||||
}));
|
||||
if (event.target.value.trim()) setNameError(false);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
{nameError && (
|
||||
<p className="text-sm text-destructive">
|
||||
{t('common.fieldRequired')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showEmoji && (
|
||||
<div className="space-y-2">
|
||||
<Label>{t('common.icon')}</Label>
|
||||
<EmojiPicker
|
||||
value={draft.emoji || defaultEmoji}
|
||||
onChange={(emoji) =>
|
||||
setDraft((current) => ({ ...current, emoji }))
|
||||
}
|
||||
ariaLabel={t('common.icon')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="entity-basic-description">
|
||||
{t('common.description')}
|
||||
</Label>
|
||||
<Input
|
||||
id="entity-basic-description"
|
||||
value={draft.description}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
description: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export default function EntityTitleEditButton({
|
||||
onClick,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 shrink-0 text-muted-foreground"
|
||||
aria-label={t('common.editBasicInfo')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('common.editBasicInfo')}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export interface ProcessorDetailStatus {
|
||||
|
||||
interface ProcessorDetailWorkbenchProps {
|
||||
title: string;
|
||||
titleAction?: ReactNode;
|
||||
status?: ProcessorDetailStatus | null;
|
||||
saveLabel: string;
|
||||
saveFormId: string;
|
||||
@@ -41,6 +42,7 @@ interface ProcessorDetailWorkbenchProps {
|
||||
|
||||
export default function ProcessorDetailWorkbench({
|
||||
title,
|
||||
titleAction,
|
||||
status,
|
||||
saveLabel,
|
||||
saveFormId,
|
||||
@@ -67,6 +69,7 @@ export default function ProcessorDetailWorkbench({
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h1 className="truncate text-xl font-semibold">{title}</h1>
|
||||
{titleAction}
|
||||
{status && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -139,7 +142,10 @@ export default function ProcessorDetailWorkbench({
|
||||
</div>
|
||||
|
||||
{activeView === 'monitoring' && monitoring ? (
|
||||
<section className="min-h-0 flex-1 overflow-y-auto rounded-xl border bg-card p-4">
|
||||
<section
|
||||
aria-label={monitoring.label}
|
||||
className="min-h-0 flex-1 overflow-y-auto rounded-xl border bg-card p-4"
|
||||
>
|
||||
{monitoring.content}
|
||||
</section>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user