| import { useTranslation } from 'react-i18next' |
| import { |
| Dialog, |
| DialogContent, |
| DialogHeader, |
| DialogTitle, |
| DialogFooter, |
| DialogDescription |
| } from '@/components/ui/Dialog' |
| import Button from '@/components/ui/Button' |
| import Checkbox from '@/components/ui/Checkbox' |
|
|
| interface PropertyEditDialogProps { |
| isOpen: boolean |
| onClose: () => void |
| onSave: () => void | Promise<void> |
| propertyName: string |
| value: string |
| allowMerge: boolean |
| onValueChange: (value: string) => void |
| onAllowMergeChange: (allowMerge: boolean) => void |
| isSubmitting?: boolean |
| errorMessage?: string | null |
| } |
|
|
| |
| |
| |
| |
| const PropertyEditDialog = ({ |
| isOpen, |
| onClose, |
| onSave, |
| propertyName, |
| value, |
| allowMerge, |
| onValueChange, |
| onAllowMergeChange, |
| isSubmitting = false, |
| errorMessage = null |
| }: PropertyEditDialogProps) => { |
| const { t } = useTranslation() |
|
|
| |
| const getPropertyNameTranslation = (name: string) => { |
| const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}` |
| const translation = t(translationKey) |
| return translation === translationKey ? name : translation |
| } |
|
|
| |
| const getTextareaConfig = (propertyName: string) => { |
| switch (propertyName) { |
| case 'description': |
| return { |
| |
| className: 'max-h-[50vh] min-h-[10em] resize-y', |
| style: { |
| height: '70vh', |
| minHeight: '20em', |
| resize: 'vertical' as const |
| } |
| }; |
| case 'entity_id': |
| return { |
| rows: 2, |
| className: '', |
| style: {} |
| }; |
| case 'keywords': |
| return { |
| rows: 4, |
| className: '', |
| style: {} |
| }; |
| default: |
| return { |
| rows: 5, |
| className: '', |
| style: {} |
| }; |
| } |
| }; |
|
|
| const handleSave = async () => { |
| const trimmedValue = value.trim() |
| if (trimmedValue !== '') { |
| await onSave() |
| } |
| } |
|
|
| return ( |
| <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> |
| <DialogContent className="sm:max-w-md"> |
| <DialogHeader> |
| <DialogTitle> |
| {t('graphPanel.propertiesView.editProperty', { |
| property: getPropertyNameTranslation(propertyName) |
| })} |
| </DialogTitle> |
| <DialogDescription> |
| {t('graphPanel.propertiesView.editPropertyDescription')} |
| </DialogDescription> |
| </DialogHeader> |
| |
| {/* Display error message if save fails */} |
| {errorMessage && ( |
| <div className="bg-destructive/15 text-destructive px-4 py-2 rounded-md text-sm"> |
| {errorMessage} |
| </div> |
| )} |
| |
| {/* Multi-line text input using textarea */} |
| <div className="grid gap-4 py-4"> |
| {(() => { |
| const config = getTextareaConfig(propertyName); |
| return propertyName === 'description' ? ( |
| <textarea |
| value={value} |
| onChange={(e) => onValueChange(e.target.value)} |
| className={`border-input focus-visible:ring-ring flex w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${config.className}`} |
| style={config.style} |
| disabled={isSubmitting} |
| /> |
| ) : ( |
| <textarea |
| value={value} |
| onChange={(e) => onValueChange(e.target.value)} |
| rows={config.rows} |
| className={`border-input focus-visible:ring-ring flex w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${config.className}`} |
| disabled={isSubmitting} |
| /> |
| ); |
| })()} |
| </div> |
| |
| {propertyName === 'entity_id' && ( |
| <div className="rounded-md border border-border bg-muted/20 p-3"> |
| <label className="flex items-start gap-2 text-sm font-medium"> |
| <Checkbox |
| id="allow-merge" |
| checked={allowMerge} |
| disabled={isSubmitting} |
| onCheckedChange={(checked) => onAllowMergeChange(checked === true)} |
| /> |
| <div> |
| <span>{t('graphPanel.propertiesView.mergeOptionLabel')}</span> |
| <p className="text-xs font-normal text-muted-foreground"> |
| {t('graphPanel.propertiesView.mergeOptionDescription')} |
| </p> |
| </div> |
| </label> |
| </div> |
| )} |
| |
| <DialogFooter> |
| <Button |
| type="button" |
| variant="outline" |
| onClick={onClose} |
| disabled={isSubmitting} |
| > |
| {t('common.cancel')} |
| </Button> |
| <Button |
| type="button" |
| onClick={handleSave} |
| disabled={isSubmitting} |
| > |
| {isSubmitting ? ( |
| <> |
| <span className="mr-2"> |
| <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
| </svg> |
| </span> |
| {t('common.saving')} |
| </> |
| ) : ( |
| t('common.save') |
| )} |
| </Button> |
| </DialogFooter> |
| </DialogContent> |
| </Dialog> |
| ) |
| } |
|
|
| export default PropertyEditDialog |
|
|