Feat: Improve metadata logic (#12730)

### What problem does this PR solve?

Feat: Improve metadata logic

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2026-01-21 11:31:26 +08:00
committed by GitHub
parent bc7935d627
commit 5a7026cf55
16 changed files with 1318 additions and 665 deletions

View File

@ -834,6 +834,7 @@ const DynamicForm = {
useImperativeHandle( useImperativeHandle(
ref, ref,
() => ({ () => ({
form: form,
submit: () => { submit: () => {
form.handleSubmit((values) => { form.handleSubmit((values) => {
const filteredValues = filterActiveValues(values); const filteredValues = filterActiveValues(values);
@ -938,7 +939,6 @@ const DynamicForm = {
) as <T extends FieldValues>( ) as <T extends FieldValues>(
props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> }, props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> },
) => React.ReactElement, ) => React.ReactElement,
SavingButton: ({ SavingButton: ({
submitLoading, submitLoading,
buttonText, buttonText,
@ -1015,4 +1015,6 @@ const DynamicForm = {
}, },
}; };
DynamicForm.Root.displayName = 'DynamicFormRoot';
export { DynamicForm }; export { DynamicForm };

View File

@ -0,0 +1,108 @@
import { Calendar } from '@/components/originui/calendar';
import { Input } from '@/components/ui/input';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Locale } from 'date-fns';
import dayjs from 'dayjs';
import { Calendar as CalendarIcon } from 'lucide-react';
import * as React from 'react';
interface DateInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange'
> {
value?: Date;
onChange?: (date: Date | undefined) => void;
showTimeSelect?: boolean;
dateFormat?: string;
timeFormat?: string;
showTimeSelectOnly?: boolean;
showTimeInput?: boolean;
timeInputLabel?: string;
locale?: Locale; // Support for internationalization
}
const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
(
{
className,
value,
onChange,
dateFormat = 'DD/MM/YYYY',
timeFormat = 'HH:mm:ss',
showTimeSelect = false,
showTimeSelectOnly = false,
showTimeInput = false,
timeInputLabel = '',
...props
},
ref,
) => {
const [open, setOpen] = React.useState(false);
const handleDateSelect = (date: Date | undefined) => {
onChange?.(date);
setOpen(false);
};
// Determine display format based on the type of date picker
let displayFormat = dateFormat;
if (showTimeSelect) {
displayFormat = `${dateFormat} ${timeFormat}`;
} else if (showTimeSelectOnly) {
displayFormat = timeFormat;
}
// Format the date according to the specified format
const formattedValue = React.useMemo(() => {
return value && !isNaN(value.getTime())
? dayjs(value).format(displayFormat)
: '';
}, [value, displayFormat]);
return (
<div className="grid gap-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div className="relative">
<Input
ref={ref}
value={formattedValue}
readOnly
className={cn(
'bg-bg-card hover:text-text-primary border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] cursor-pointer',
className,
)}
{...props}
/>
<CalendarIcon
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors"
size={16}
/>
</div>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<Calendar
mode="single"
selected={value}
onSelect={handleDateSelect}
initialFocus
{...(showTimeSelect && {
showTimeInput,
timeInputLabel,
})}
/>
</PopoverContent>
</Popover>
</div>
);
},
);
DateInput.displayName = 'DateInput';
export { DateInput };

View File

@ -1,5 +1,6 @@
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -17,10 +18,12 @@ export interface InputSelectOption {
export interface InputSelectProps { export interface InputSelectProps {
/** Options for the select component */ /** Options for the select component */
options?: InputSelectOption[]; options?: InputSelectOption[];
/** Selected values - string for single select, array for multi select */ /** Selected values - type depends on the input type */
value?: string | string[]; value?: string | string[] | number | number[] | Date | Date[];
/** Callback when value changes */ /** Callback when value changes */
onChange?: (value: string | string[]) => void; onChange?: (
value: string | string[] | number | number[] | Date | Date[],
) => void;
/** Placeholder text */ /** Placeholder text */
placeholder?: string; placeholder?: string;
/** Additional class names */ /** Additional class names */
@ -29,6 +32,8 @@ export interface InputSelectProps {
style?: React.CSSProperties; style?: React.CSSProperties;
/** Whether to allow multiple selections */ /** Whether to allow multiple selections */
multi?: boolean; multi?: boolean;
/** Type of input: text, number, date, or datetime */
type?: 'text' | 'number' | 'date' | 'datetime';
} }
const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>( const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
@ -41,6 +46,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
className, className,
style, style,
multi = false, multi = false,
type = 'text',
}, },
ref, ref,
) => { ) => {
@ -50,36 +56,108 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
// Normalize value to array for consistent handling // Normalize value to array for consistent handling based on type
const normalizedValue = Array.isArray(value) ? value : value ? [value] : []; const normalizedValue = React.useMemo(() => {
if (Array.isArray(value)) {
return value;
} else if (value !== undefined && value !== null) {
if (type === 'number') {
return typeof value === 'number' ? [value] : [Number(value)];
} else if (type === 'date' || type === 'datetime') {
return value instanceof Date ? [value] : [new Date(value as any)];
} else {
return typeof value === 'string' ? [value] : [String(value)];
}
} else {
return [];
}
}, [value, type]);
/** /**
* Removes a tag from the selected values * Removes a tag from the selected values
* @param tagValue - The value of the tag to remove * @param tagValue - The value of the tag to remove
*/ */
const handleRemoveTag = (tagValue: string) => { const handleRemoveTag = (tagValue: any) => {
const newValue = normalizedValue.filter((v) => v !== tagValue); let newValue: any[];
if (type === 'number') {
newValue = (normalizedValue as number[]).filter((v) => v !== tagValue);
} else if (type === 'date' || type === 'datetime') {
newValue = (normalizedValue as Date[]).filter(
(v) => v.getTime() !== tagValue.getTime(),
);
} else {
newValue = (normalizedValue as string[]).filter((v) => v !== tagValue);
}
// Return single value if not multi-select, otherwise return array // Return single value if not multi-select, otherwise return array
onChange?.(multi ? newValue : newValue[0] || ''); let result: string | number | Date | string[] | number[] | Date[];
if (multi) {
result = newValue;
} else {
if (type === 'number') {
result = newValue[0] || 0;
} else if (type === 'date' || type === 'datetime') {
result = newValue[0] || new Date();
} else {
result = newValue[0] || '';
}
}
onChange?.(result);
}; };
/** /**
* Adds a tag to the selected values * Adds a tag to the selected values
* @param optionValue - The value of the tag to add * @param optionValue - The value of the tag to add
*/ */
const handleAddTag = (optionValue: string) => { const handleAddTag = (optionValue: any) => {
let newValue: string[]; let newValue: any[];
if (multi) { if (multi) {
// For multi-select, add to array if not already included // For multi-select, add to array if not already included
if (!normalizedValue.includes(optionValue)) { if (type === 'number') {
newValue = [...normalizedValue, optionValue]; const numValue =
onChange?.(newValue); typeof optionValue === 'number' ? optionValue : Number(optionValue);
if (
!(normalizedValue as number[]).includes(numValue) &&
!isNaN(numValue)
) {
newValue = [...(normalizedValue as number[]), numValue];
onChange?.(newValue as number[]);
}
} else if (type === 'date' || type === 'datetime') {
const dateValue =
optionValue instanceof Date ? optionValue : new Date(optionValue);
if (
!(normalizedValue as Date[]).some(
(d) => d.getTime() === dateValue.getTime(),
)
) {
newValue = [...(normalizedValue as Date[]), dateValue];
onChange?.(newValue as Date[]);
}
} else {
if (!(normalizedValue as string[]).includes(optionValue)) {
newValue = [...(normalizedValue as string[]), optionValue];
onChange?.(newValue as string[]);
}
} }
} else { } else {
// For single-select, replace the value // For single-select, replace the value
newValue = [optionValue]; if (type === 'number') {
onChange?.(optionValue); const numValue =
typeof optionValue === 'number' ? optionValue : Number(optionValue);
if (!isNaN(numValue)) {
onChange?.(numValue);
}
} else if (type === 'date' || type === 'datetime') {
const dateValue =
optionValue instanceof Date ? optionValue : new Date(optionValue);
onChange?.(dateValue);
} else {
onChange?.(optionValue);
}
} }
setInputValue(''); setInputValue('');
@ -89,16 +167,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value; const newValue = e.target.value;
setInputValue(newValue); setInputValue(newValue);
setOpen(newValue.length > 0); // Open popover when there's input setOpen(!!newValue); // Open popover when there's input
// If input matches an option exactly, add it
const matchedOption = options.find(
(opt) => opt.label.toLowerCase() === newValue.toLowerCase(),
);
if (matchedOption && !normalizedValue.includes(matchedOption.value)) {
handleAddTag(matchedOption.value);
}
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@ -111,9 +180,37 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
const newValue = [...normalizedValue]; const newValue = [...normalizedValue];
newValue.pop(); newValue.pop();
// Return single value if not multi-select, otherwise return array // Return single value if not multi-select, otherwise return array
onChange?.(multi ? newValue : newValue[0] || ''); let result: string | number | Date | string[] | number[] | Date[];
if (multi) {
result = newValue;
} else {
if (type === 'number') {
result = newValue[0] || 0;
} else if (type === 'date' || type === 'datetime') {
result = newValue[0] || new Date();
} else {
result = newValue[0] || '';
}
}
onChange?.(result);
} else if (e.key === 'Enter' && inputValue.trim() !== '') { } else if (e.key === 'Enter' && inputValue.trim() !== '') {
e.preventDefault(); e.preventDefault();
let valueToAdd: any;
if (type === 'number') {
const numValue = Number(inputValue);
if (isNaN(numValue)) return; // Don't add invalid numbers
valueToAdd = numValue;
} else if (type === 'date' || type === 'datetime') {
const dateValue = new Date(inputValue);
if (isNaN(dateValue.getTime())) return; // Don't add invalid dates
valueToAdd = dateValue;
} else {
valueToAdd = inputValue;
}
// Add input value as a new tag if it doesn't exist in options // Add input value as a new tag if it doesn't exist in options
const matchedOption = options.find( const matchedOption = options.find(
(opt) => opt.label.toLowerCase() === inputValue.toLowerCase(), (opt) => opt.label.toLowerCase() === inputValue.toLowerCase(),
@ -124,10 +221,16 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
} else { } else {
// If not in options, create a new tag with the input value // If not in options, create a new tag with the input value
if ( if (
!normalizedValue.includes(inputValue) && !normalizedValue.some((v) =>
type === 'number'
? Number(v) === Number(valueToAdd)
: type === 'date' || type === 'datetime'
? new Date(v as any).getTime() === valueToAdd.getTime()
: String(v) === valueToAdd,
) &&
inputValue.trim() !== '' inputValue.trim() !== ''
) { ) {
handleAddTag(inputValue); handleAddTag(valueToAdd);
} }
} }
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
@ -160,26 +263,68 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
// Filter options to exclude already selected ones (only for multi-select) // Filter options to exclude already selected ones (only for multi-select)
const availableOptions = multi const availableOptions = multi
? options.filter((option) => !normalizedValue.includes(option.value)) ? options.filter(
(option) =>
!normalizedValue.some((v) =>
type === 'number'
? Number(v) === Number(option.value)
: type === 'date' || type === 'datetime'
? new Date(v as any).getTime() ===
new Date(option.value).getTime()
: String(v) === option.value,
),
)
: options; : options;
const filteredOptions = availableOptions.filter( const filteredOptions = availableOptions.filter(
(option) => (option) =>
!inputValue || !inputValue ||
option.label.toLowerCase().includes(inputValue.toLowerCase()), option.label
.toLowerCase()
.includes(inputValue.toString().toLowerCase()),
); );
// If there are no matching options but there is an input value, create a new option with the input value // If there are no matching options but there is an input value, create a new option with the input value
const hasMatchingOptions = filteredOptions.length > 0; const showInputAsOption = React.useMemo(() => {
const showInputAsOption = if (!inputValue) return false;
inputValue &&
!hasMatchingOptions && const hasLabelMatch = options.some(
!normalizedValue.includes(inputValue); (option) =>
option.label.toLowerCase() === inputValue.toString().toLowerCase(),
);
let isAlreadySelected = false;
if (type === 'number') {
const numValue = Number(inputValue);
isAlreadySelected =
!isNaN(numValue) && (normalizedValue as number[]).includes(numValue);
} else if (type === 'date' || type === 'datetime') {
const dateValue = new Date(inputValue);
isAlreadySelected =
!isNaN(dateValue.getTime()) &&
(normalizedValue as Date[]).some(
(d) => d.getTime() === dateValue.getTime(),
);
} else {
isAlreadySelected = (normalizedValue as string[]).includes(inputValue);
}
console.log(
'showInputAsOption',
hasLabelMatch,
isAlreadySelected,
inputValue.toString().trim(),
);
return (
!hasLabelMatch &&
!isAlreadySelected &&
inputValue.toString().trim() !== ''
);
}, [inputValue, options, normalizedValue, type]);
const triggerElement = ( const triggerElement = (
<div <div
className={cn( className={cn(
'flex flex-wrap items-center gap-1 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-2 min-h-[40px] cursor-text', 'flex flex-wrap items-center gap-1 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-1 min-h-8 cursor-text',
'outline-none transition-colors', 'outline-none transition-colors',
'focus-within:outline-none focus-within:ring-1 focus-within:ring-accent-primary', 'focus-within:outline-none focus-within:ring-1 focus-within:ring-accent-primary',
className, className,
@ -189,14 +334,22 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
> >
{/* Render selected tags - only show tags if multi is true or if single select has a value */} {/* Render selected tags - only show tags if multi is true or if single select has a value */}
{multi && {multi &&
normalizedValue.map((tagValue) => { normalizedValue.map((tagValue, index) => {
const option = options.find((opt) => opt.value === tagValue) || { const option = options.find((opt) =>
value: tagValue, type === 'number'
label: tagValue, ? Number(opt.value) === Number(tagValue)
: type === 'date' || type === 'datetime'
? new Date(opt.value).getTime() ===
new Date(tagValue).getTime()
: String(opt.value) === String(tagValue),
) || {
value: String(tagValue),
label: String(tagValue),
}; };
return ( return (
<div <div
key={tagValue} key={`${tagValue}-${index}`}
className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card" className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card"
> >
{option.label} {option.label}
@ -215,11 +368,22 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
})} })}
{/* For single select, show the selected value as text instead of a tag */} {/* For single select, show the selected value as text instead of a tag */}
{!multi && normalizedValue[0] && ( {!multi && !isEmpty(normalizedValue[0]) && (
<div className="flex items-center mr-2 max-w-full"> <div className={cn('flex items-center max-w-full')}>
<div className="flex-1 truncate"> <div className="flex-1 truncate">
{options.find((opt) => opt.value === normalizedValue[0])?.label || {options.find((opt) =>
normalizedValue[0]} type === 'number'
? Number(opt.value) === Number(normalizedValue[0])
: type === 'date' || type === 'datetime'
? new Date(opt.value).getTime() ===
new Date(normalizedValue[0]).getTime()
: String(opt.value) === String(normalizedValue[0]),
)?.label ||
(type === 'number'
? String(normalizedValue[0])
: type === 'date' || type === 'datetime'
? new Date(normalizedValue[0] as any).toLocaleString()
: String(normalizedValue[0]))}
</div> </div>
<button <button
type="button" type="button"
@ -235,19 +399,37 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
)} )}
{/* Input field for adding new tags - hide if single select and value is already selected, or in multi select when not focused */} {/* Input field for adding new tags - hide if single select and value is already selected, or in multi select when not focused */}
{(multi ? isFocused : multi || !normalizedValue[0]) && ( {(multi ? isFocused : multi || isEmpty(normalizedValue[0])) && (
<Input <Input
ref={inputRef} ref={inputRef}
type="text" type={
value={inputValue} type === 'date'
? 'date'
: type === 'datetime'
? 'datetime-local'
: type === 'number'
? 'number'
: 'text'
}
value={
type === 'number' && inputValue
? String(inputValue)
: type === 'date' || type === 'datetime'
? inputValue
: inputValue
}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={ placeholder={
(multi ? normalizedValue.length === 0 : !normalizedValue[0]) (
multi
? normalizedValue.length === 0
: isEmpty(normalizedValue[0])
)
? placeholder ? placeholder
: '' : ''
} }
className="flex-grow min-w-[50px] border-none px-1 py-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-auto !w-fit" className="flex-grow min-w-[50px] border-none px-1 py-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-auto "
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onFocus={handleInputFocus} onFocus={handleInputFocus}
onBlur={handleInputBlur} onBlur={handleInputBlur}
@ -272,7 +454,19 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
<div <div
key={option.value} key={option.value}
className="px-4 py-2 hover:bg-border-button cursor-pointer text-text-secondary w-full truncate" className="px-4 py-2 hover:bg-border-button cursor-pointer text-text-secondary w-full truncate"
onClick={() => handleAddTag(option.value)} onClick={() => {
let optionValue: any;
if (type === 'number') {
optionValue = Number(option.value);
if (isNaN(optionValue)) return; // Skip invalid numbers
} else if (type === 'date' || type === 'datetime') {
optionValue = new Date(option.value);
if (isNaN(optionValue.getTime())) return; // Skip invalid dates
} else {
optionValue = option.value;
}
handleAddTag(optionValue);
}}
> >
{option.label} {option.label}
</div> </div>
@ -281,9 +475,17 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
<div <div
key={inputValue} key={inputValue}
className="px-4 py-2 hover:bg-border-button cursor-pointer text-text-secondary w-full truncate" className="px-4 py-2 hover:bg-border-button cursor-pointer text-text-secondary w-full truncate"
onClick={() => handleAddTag(inputValue)} onClick={() =>
handleAddTag(
type === 'number'
? Number(inputValue)
: type === 'date' || type === 'datetime'
? new Date(inputValue)
: inputValue,
)
}
> >
{t('common.add')} &quot;{inputValue}&#34; {t('common.add')} &quot;{inputValue}&quot;
</div> </div>
)} )}
{filteredOptions.length === 0 && !showInputAsOption && ( {filteredOptions.length === 0 && !showInputAsOption && (

View File

@ -274,7 +274,6 @@ export const useSendMessageWithSse = (
const val = JSON.parse(value?.data || ''); const val = JSON.parse(value?.data || '');
const d = val?.data; const d = val?.data;
if (typeof d !== 'boolean') { if (typeof d !== 'boolean') {
setAnswer((prev) => { setAnswer((prev) => {
const prevAnswer = prev.answer || ''; const prevAnswer = prev.answer || '';
const currentAnswer = d.answer || ''; const currentAnswer = d.answer || '';

View File

@ -184,6 +184,10 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
}, },
knowledgeDetails: { knowledgeDetails: {
metadata: { metadata: {
type: 'Type',
fieldNameInvalid: 'Field name can only contain letters or underscores.',
builtIn: 'Built-in',
generation: 'Generation',
toMetadataSetting: 'Generation settings', toMetadataSetting: 'Generation settings',
toMetadataSettingTip: 'Set auto-metadata in Configuration.', toMetadataSettingTip: 'Set auto-metadata in Configuration.',
descriptionTip: descriptionTip:
@ -289,7 +293,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
localFiles: 'Local files', localFiles: 'Local files',
emptyFiles: 'Create empty file', emptyFiles: 'Create empty file',
webCrawl: 'Web crawl', webCrawl: 'Web crawl',
chunkNumber: 'Chunk number', chunkNumber: 'Chunks',
uploadDate: 'Upload date', uploadDate: 'Upload date',
chunkMethod: 'Chunking method', chunkMethod: 'Chunking method',
enabled: 'Enable', enabled: 'Enable',

View File

@ -175,6 +175,10 @@ export default {
}, },
knowledgeDetails: { knowledgeDetails: {
metadata: { metadata: {
type: '类型',
fieldNameInvalid: '字段名称只能包含字母或下划线。',
builtIn: '内置',
generation: '生成',
toMetadataSettingTip: '在配置中设置自动元数据', toMetadataSettingTip: '在配置中设置自动元数据',
toMetadataSetting: '生成设置', toMetadataSetting: '生成设置',
descriptionTip: descriptionTip:

View File

@ -1,5 +1,6 @@
import message from '@/components/ui/message'; import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useSelectedIds } from '@/hooks/logic-hooks/use-row-selection';
import { import {
DocumentApiAction, DocumentApiAction,
useSetDocumentMeta, useSetDocumentMeta,
@ -9,13 +10,13 @@ import kbService, {
updateMetaData, updateMetaData,
} from '@/services/knowledge-service'; } from '@/services/knowledge-service';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { RowSelectionState } from '@tanstack/react-table';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { import {
IBuiltInMetadataItem, IBuiltInMetadataItem,
IMetaDataJsonSchemaProperty,
IMetaDataReturnJSONSettings, IMetaDataReturnJSONSettings,
IMetaDataReturnJSONType, IMetaDataReturnJSONType,
IMetaDataReturnType, IMetaDataReturnType,
@ -76,14 +77,16 @@ export const MetadataDeleteMap = (
}; };
const DEFAULT_VALUE_TYPE: MetadataValueType = 'string'; const DEFAULT_VALUE_TYPE: MetadataValueType = 'string';
const VALUE_TYPES_WITH_ENUM = new Set<MetadataValueType>(['enum']); // const VALUE_TYPES_WITH_ENUM = new Set<MetadataValueType>(['enum']);
const VALUE_TYPE_LABELS: Record<MetadataValueType, string> = { const VALUE_TYPE_LABELS: Record<MetadataValueType, string> = {
string: 'String', string: 'String',
bool: 'Bool',
enum: 'Enum',
time: 'Time', time: 'Time',
int: 'Int', number: 'Number',
float: 'Float', // bool: 'Bool',
// enum: 'Enum',
// 'list<string>': 'List<String>',
// int: 'Int',
// float: 'Float',
}; };
export const metadataValueTypeOptions = Object.entries(VALUE_TYPE_LABELS).map( export const metadataValueTypeOptions = Object.entries(VALUE_TYPE_LABELS).map(
@ -93,82 +96,101 @@ export const metadataValueTypeOptions = Object.entries(VALUE_TYPE_LABELS).map(
export const getMetadataValueTypeLabel = (value?: MetadataValueType) => export const getMetadataValueTypeLabel = (value?: MetadataValueType) =>
VALUE_TYPE_LABELS[value || DEFAULT_VALUE_TYPE] || VALUE_TYPE_LABELS.string; VALUE_TYPE_LABELS[value || DEFAULT_VALUE_TYPE] || VALUE_TYPE_LABELS.string;
export const isMetadataValueTypeWithEnum = (value?: MetadataValueType) => // export const isMetadataValueTypeWithEnum = (value?: MetadataValueType) =>
VALUE_TYPES_WITH_ENUM.has(value || DEFAULT_VALUE_TYPE); // VALUE_TYPES_WITH_ENUM.has(value || DEFAULT_VALUE_TYPE);
const schemaToValueType = ( // const schemaToValueType = (
property?: IMetaDataJsonSchemaProperty, // property?: IMetaDataJsonSchemaProperty,
): MetadataValueType => { // ): MetadataValueType => {
if (!property) return DEFAULT_VALUE_TYPE; // if (!property) return DEFAULT_VALUE_TYPE;
if ( // if (
property.type === 'array' && // property.type === 'array' &&
property.items?.type === 'string' && // property.items?.type === 'string' &&
(property.items.enum?.length || 0) > 0 // (property.items.enum?.length || 0) > 0
) { // ) {
return 'enum'; // return 'enum';
} // }
if (property.type === 'boolean') return 'bool'; // if (property.type === 'boolean') return 'bool';
if (property.type === 'integer') return 'int'; // if (property.type === 'integer') return 'int';
if (property.type === 'number') return 'float'; // if (property.type === 'number') return 'float';
if (property.type === 'string' && property.format) { // if (property.type === 'string' && property.format) {
return 'time'; // return 'time';
} // }
if (property.type === 'string' && property.enum?.length) { // if (property.type === 'string' && property.enum?.length) {
return 'enum'; // return 'enum';
} // }
return DEFAULT_VALUE_TYPE; // return DEFAULT_VALUE_TYPE;
}; // };
const valueTypeToSchema = ( // const valueTypeToSchema = (
valueType: MetadataValueType, // valueType: MetadataValueType,
description: string, // description: string,
values: string[], // values: string[],
): IMetaDataJsonSchemaProperty => { // ): IMetaDataJsonSchemaProperty => {
const schema: IMetaDataJsonSchemaProperty = { // const schema: IMetaDataJsonSchemaProperty = {
description: description || '', // description: description || '',
}; // };
switch (valueType) { // switch (valueType) {
case 'bool': // case 'bool':
schema.type = 'boolean'; // schema.type = 'boolean';
return schema; // return schema;
case 'int': // case 'int':
schema.type = 'integer'; // schema.type = 'integer';
return schema; // return schema;
case 'float': // case 'float':
schema.type = 'number'; // schema.type = 'number';
return schema; // return schema;
case 'time': // case 'time':
schema.type = 'string'; // schema.type = 'string';
schema.format = 'date-time'; // schema.format = 'date-time';
return schema; // return schema;
case 'enum': // case 'enum':
schema.type = 'string'; // schema.type = 'string';
if (values?.length) { // if (values?.length) {
schema.enum = values; // schema.enum = values;
} // }
return schema; // return schema;
case 'string': // case 'string':
default: // default:
schema.type = 'string'; // schema.type = 'string';
if (values?.length) { // if (values?.length) {
schema.enum = values; // schema.enum = values;
} // }
return schema; // return schema;
} // }
}; // };
export const util = { export const util = {
changeToMetaDataTableData(data: IMetaDataReturnType): IMetaDataTableData[] { changeToMetaDataTableData(data: IMetaDataReturnType): IMetaDataTableData[] {
return Object.entries(data).map(([key, value]) => { const res = Object.entries(data).map(
const values = value.map(([v]) => v.toString()); ([key, value]: [
console.log('values', values); string,
return { (
field: key, | { type: string; values: Array<Array<string | number>> }
description: '', | Array<Array<string | number>>
values: values, ),
} as IMetaDataTableData; ]) => {
}); if (Array.isArray(value)) {
const values = value.map(([v]) => v.toString());
return {
field: key,
valueType: DEFAULT_VALUE_TYPE,
description: '',
values: values,
} as IMetaDataTableData;
}
const { type, values } = value;
const valueArr = values.map(([v]) => v.toString());
return {
field: key,
valueType: type,
description: '',
values: valueArr,
} as IMetaDataTableData;
},
);
return res;
}, },
JSONToMetaDataTableData( JSONToMetaDataTableData(
@ -204,31 +226,13 @@ export const util = {
tableDataToMetaDataSettingJSON( tableDataToMetaDataSettingJSON(
data: IMetaDataTableData[], data: IMetaDataTableData[],
): IMetaDataReturnJSONSettings { ): IMetaDataReturnJSONSettings {
const properties = data.reduce<Record<string, IMetaDataJsonSchemaProperty>>( return data.map((item) => {
(acc, item) => { return {
if (!item.field) { key: item.field,
return acc; description: item.description,
} enum: item.values,
const valueType = item.valueType || DEFAULT_VALUE_TYPE; };
const values = });
isMetadataValueTypeWithEnum(valueType) && item.restrictDefinedValues
? item.values
: [];
acc[item.field] = valueTypeToSchema(
valueType,
item.description,
values,
);
return acc;
},
{},
);
return {
type: 'object',
properties,
additionalProperties: false,
};
}, },
metaDataSettingJSONToMetaDataTableData( metaDataSettingJSONToMetaDataTableData(
@ -248,7 +252,7 @@ export const util = {
} }
const properties = data.properties || {}; const properties = data.properties || {};
return Object.entries(properties).map(([key, property]) => { return Object.entries(properties).map(([key, property]) => {
const valueType = schemaToValueType(property); const valueType = 'string';
const values = property.enum || property.items?.enum || []; const values = property.enum || property.items?.enum || [];
return { return {
field: key, field: key,
@ -274,6 +278,13 @@ export const useMetadataOperations = () => {
})); }));
}, []); }, []);
const addDeleteBatch = useCallback((keys: string[]) => {
setOperations((prev) => ({
...prev,
deletes: [...prev.deletes, ...keys.map((key) => ({ key }))],
}));
}, []);
const addDeleteValue = useCallback((key: string, value: string) => { const addDeleteValue = useCallback((key: string, value: string) => {
setOperations((prev) => ({ setOperations((prev) => ({
...prev, ...prev,
@ -281,15 +292,6 @@ export const useMetadataOperations = () => {
})); }));
}, []); }, []);
// const addUpdateValue = useCallback(
// (key: string, value: string | string[]) => {
// setOperations((prev) => ({
// ...prev,
// updates: [...prev.updates, { key, value }],
// }));
// },
// [],
// );
const addUpdateValue = useCallback( const addUpdateValue = useCallback(
(key: string, originalValue: string, newValue: string) => { (key: string, originalValue: string, newValue: string) => {
setOperations((prev) => { setOperations((prev) => {
@ -330,6 +332,7 @@ export const useMetadataOperations = () => {
return { return {
operations, operations,
addDeleteBatch,
addDeleteRow, addDeleteRow,
addDeleteValue, addDeleteValue,
addUpdateValue, addUpdateValue,
@ -339,48 +342,29 @@ export const useMetadataOperations = () => {
export const useFetchMetaDataManageData = ( export const useFetchMetaDataManageData = (
type: MetadataType = MetadataType.Manage, type: MetadataType = MetadataType.Manage,
documentIds?: string[],
) => { ) => {
const { id } = useParams(); const { id } = useParams();
// const [data, setData] = useState<IMetaDataTableData[]>([]);
// const [loading, setLoading] = useState(false);
// const fetchData = useCallback(async (): Promise<IMetaDataTableData[]> => {
// setLoading(true);
// const { data } = await getMetaDataService({
// kb_id: id as string,
// });
// setLoading(false);
// if (data?.data?.summary) {
// return util.changeToMetaDataTableData(data.data.summary);
// }
// return [];
// }, [id]);
// useEffect(() => {
// if (type === MetadataType.Manage) {
// fetchData()
// .then((res) => {
// setData(res);
// })
// .catch((res) => {
// console.error(res);
// });
// }
// }, [type, fetchData]);
const { const {
data, data,
isFetching: loading, isFetching: loading,
refetch, refetch,
} = useQuery<IMetaDataTableData[]>({ } = useQuery<IMetaDataTableData[]>({
queryKey: ['fetchMetaData', id], queryKey: ['fetchMetaData', id, documentIds],
enabled: !!id && type === MetadataType.Manage, enabled:
!!id &&
(type === MetadataType.Manage || type === MetadataType.UpdateSingle),
initialData: [], initialData: [],
gcTime: 1000, gcTime: 1000,
queryFn: async () => { queryFn: async () => {
const { data } = await getMetaDataService({ const { data } = await getMetaDataService({
kb_id: id as string, kb_id: id as string,
doc_ids: documentIds,
}); });
if (data?.data?.summary) { if (data?.data?.summary) {
return util.changeToMetaDataTableData(data.data.summary); const res = util.changeToMetaDataTableData(data.data.summary);
return res;
} }
return []; return [];
}, },
@ -392,20 +376,23 @@ export const useFetchMetaDataManageData = (
}; };
}; };
const fetchTypeList = [MetadataType.Manage, MetadataType.UpdateSingle];
export const useManageMetaDataModal = ( export const useManageMetaDataModal = (
metaData: IMetaDataTableData[] = [], metaData: IMetaDataTableData[] = [],
type: MetadataType = MetadataType.Manage, type: MetadataType = MetadataType.Manage,
otherData?: Record<string, any>, otherData?: Record<string, any>,
documentIds?: string[],
) => { ) => {
const { id } = useParams(); const { id } = useParams();
const { t } = useTranslation(); const { t } = useTranslation();
const { data, loading } = useFetchMetaDataManageData(type); const { data, loading } = useFetchMetaDataManageData(type, documentIds);
const [tableData, setTableData] = useState<IMetaDataTableData[]>(metaData); const [tableData, setTableData] = useState<IMetaDataTableData[]>(metaData);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const {
operations, operations,
addDeleteRow, addDeleteRow,
addDeleteBatch,
addDeleteValue, addDeleteValue,
addUpdateValue, addUpdateValue,
resetOperations, resetOperations,
@ -414,7 +401,7 @@ export const useManageMetaDataModal = (
const { setDocumentMeta } = useSetDocumentMeta(); const { setDocumentMeta } = useSetDocumentMeta();
useEffect(() => { useEffect(() => {
if (type === MetadataType.Manage) { if (fetchTypeList.includes(type)) {
if (data) { if (data) {
setTableData(data); setTableData(data);
} else { } else {
@ -424,7 +411,7 @@ export const useManageMetaDataModal = (
}, [data, type]); }, [data, type]);
useEffect(() => { useEffect(() => {
if (type !== MetadataType.Manage) { if (!fetchTypeList.includes(type)) {
if (metaData) { if (metaData) {
setTableData(metaData); setTableData(metaData);
} else { } else {
@ -447,7 +434,6 @@ export const useManageMetaDataModal = (
} }
return item; return item;
}); });
// console.log('newTableData', newTableData, prevTableData);
return newTableData; return newTableData;
}); });
}, },
@ -461,18 +447,31 @@ export const useManageMetaDataModal = (
const newTableData = prevTableData.filter( const newTableData = prevTableData.filter(
(item) => item.field !== field, (item) => item.field !== field,
); );
// console.log('newTableData', newTableData, prevTableData);
return newTableData; return newTableData;
}); });
}, },
[addDeleteRow], [addDeleteRow],
); );
const handleDeleteBatchRow = useCallback(
(fields: string[]) => {
addDeleteBatch(fields);
setTableData((prevTableData) => {
const newTableData = prevTableData.filter(
(item) => !fields.includes(item.field),
);
return newTableData;
});
},
[addDeleteBatch],
);
const handleSaveManage = useCallback( const handleSaveManage = useCallback(
async (callback: () => void) => { async (callback: () => void) => {
const { data: res } = await updateMetaData({ const { data: res } = await updateMetaData({
kb_id: id as string, kb_id: id as string,
data: operations, data: operations,
doc_ids: documentIds,
}); });
if (res.code === 0) { if (res.code === 0) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@ -483,7 +482,7 @@ export const useManageMetaDataModal = (
callback(); callback();
} }
}, },
[operations, id, t, queryClient, resetOperations], [operations, id, t, queryClient, resetOperations, documentIds],
); );
const handleSaveUpdateSingle = useCallback( const handleSaveUpdateSingle = useCallback(
@ -506,13 +505,22 @@ export const useManageMetaDataModal = (
const handleSaveSettings = useCallback( const handleSaveSettings = useCallback(
async (callback: () => void, builtInMetadata?: IBuiltInMetadataItem[]) => { async (callback: () => void, builtInMetadata?: IBuiltInMetadataItem[]) => {
const data = util.tableDataToMetaDataSettingJSON(tableData); const data = util.tableDataToMetaDataSettingJSON(tableData);
callback?.(); const { data: res } = await kbService.kbUpdateMetaData({
kb_id: id,
metadata: data,
builtInMetadata: builtInMetadata || [],
});
if (res.code === 0) {
message.success(t('message.operated'));
callback?.();
}
// callback?.();
return { return {
metadata: data, metadata: data,
builtInMetadata: builtInMetadata || [], builtInMetadata: builtInMetadata || [],
}; };
}, },
[tableData], [tableData, t, id],
); );
const handleSaveSingleFileSettings = useCallback( const handleSaveSingleFileSettings = useCallback(
@ -540,17 +548,19 @@ export const useManageMetaDataModal = (
builtInMetadata, builtInMetadata,
}: { }: {
callback: () => void; callback: () => void;
builtInMetadata?: string[]; builtInMetadata?: IBuiltInMetadataItem[];
}) => { }) => {
switch (type) { switch (type) {
case MetadataType.UpdateSingle: case MetadataType.UpdateSingle:
handleSaveUpdateSingle(callback); // handleSaveUpdateSingle(callback);
handleSaveManage(callback);
break; break;
case MetadataType.Manage: case MetadataType.Manage:
handleSaveManage(callback); handleSaveManage(callback);
break; break;
case MetadataType.Setting: case MetadataType.Setting:
return handleSaveSettings(callback, builtInMetadata); return handleSaveSettings(callback, builtInMetadata);
case MetadataType.SingleFileSetting: case MetadataType.SingleFileSetting:
return handleSaveSingleFileSettings(callback); return handleSaveSingleFileSettings(callback);
default: default:
@ -561,7 +571,7 @@ export const useManageMetaDataModal = (
[ [
handleSaveManage, handleSaveManage,
type, type,
handleSaveUpdateSingle, // handleSaveUpdateSingle,
handleSaveSettings, handleSaveSettings,
handleSaveSingleFileSettings, handleSaveSingleFileSettings,
], ],
@ -572,6 +582,7 @@ export const useManageMetaDataModal = (
setTableData, setTableData,
handleDeleteSingleValue, handleDeleteSingleValue,
handleDeleteSingleRow, handleDeleteSingleRow,
handleDeleteBatchRow,
loading, loading,
handleSave, handleSave,
addUpdateValue, addUpdateValue,
@ -593,17 +604,8 @@ export const useManageMetadata = () => {
(config?: ShowManageMetadataModalProps) => { (config?: ShowManageMetadataModalProps) => {
const { metadata } = config || {}; const { metadata } = config || {};
if (metadata) { if (metadata) {
// const dataTemp = Object.entries(metadata).map(([key, value]) => {
// return {
// field: key,
// description: '',
// values: Array.isArray(value) ? value : [value],
// } as IMetaDataTableData;
// });
setTableData(metadata); setTableData(metadata);
console.log('metadata-2', metadata);
} }
console.log('metadata-3', metadata);
if (config) { if (config) {
setConfig(config); setConfig(config);
} }
@ -619,3 +621,35 @@ export const useManageMetadata = () => {
config, config,
}; };
}; };
export const useOperateData = ({
rowSelection,
list,
handleDeleteBatchRow,
}: {
rowSelection: RowSelectionState;
list: IMetaDataTableData[];
handleDeleteBatchRow: (keys: string[]) => void;
}) => {
const mapList = useMemo(() => {
return list.map((x) => {
return { ...x, id: x.field };
});
}, [list]);
const { selectedIds: selectedRowKeys } = useSelectedIds(
rowSelection,
mapList,
);
// const handleDelete = useCallback(() => {
// console.log('rowSelection', rowSelection);
// }, [rowSelection]);
const handleDelete = useCallback(() => {
const deletedKeys = selectedRowKeys.filter((x) =>
mapList.some((y) => y.id === x),
);
handleDeleteBatchRow(deletedKeys);
return;
}, [selectedRowKeys, mapList, handleDeleteBatchRow]);
return { handleDelete };
};

View File

@ -1,10 +1,6 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { MetadataDeleteMap, MetadataType } from '../hooks/use-manage-modal';
isMetadataValueTypeWithEnum,
MetadataDeleteMap,
MetadataType,
} from '../hooks/use-manage-modal';
import { IManageValuesProps, IMetaDataTableData } from '../interface'; import { IManageValuesProps, IMetaDataTableData } from '../interface';
export const useManageValues = (props: IManageValuesProps) => { export const useManageValues = (props: IManageValuesProps) => {
@ -48,7 +44,7 @@ export const useManageValues = (props: IManageValuesProps) => {
// Use functional update to avoid closure issues // Use functional update to avoid closure issues
const handleChange = useCallback( const handleChange = useCallback(
(field: string, value: any) => { async (field: string, value: any) => {
if (field === 'field' && existsKeys.includes(value)) { if (field === 'field' && existsKeys.includes(value)) {
setValueError((prev) => { setValueError((prev) => {
return { return {
@ -71,17 +67,16 @@ export const useManageValues = (props: IManageValuesProps) => {
if (field === 'valueType') { if (field === 'valueType') {
const nextValueType = (value || const nextValueType = (value ||
'string') as IMetaDataTableData['valueType']; 'string') as IMetaDataTableData['valueType'];
const supportsEnum = isMetadataValueTypeWithEnum(nextValueType);
if (!supportsEnum) { // const supportsEnum = isMetadataValueTypeWithEnum(nextValueType);
setTempValues([]); // if (!supportsEnum) {
} // setTempValues([]);
// }
return { return {
...prev, ...prev,
valueType: nextValueType, valueType: nextValueType,
values: supportsEnum ? prev.values : [], values: prev.values || [],
restrictDefinedValues: supportsEnum restrictDefinedValues: prev.restrictDefinedValues,
? prev.restrictDefinedValues || nextValueType === 'enum'
: false,
}; };
} }
return { return {
@ -89,6 +84,7 @@ export const useManageValues = (props: IManageValuesProps) => {
[field]: value, [field]: value,
}; };
}); });
return true;
}, },
[existsKeys, type, t], [existsKeys, type, t],
); );
@ -113,23 +109,46 @@ export const useManageValues = (props: IManageValuesProps) => {
if (type === MetadataType.Setting && valueError.field) { if (type === MetadataType.Setting && valueError.field) {
return; return;
} }
const supportsEnum = isMetadataValueTypeWithEnum(metaData.valueType); // const supportsEnum = isMetadataValueTypeWithEnum(metaData.valueType);
if (!supportsEnum) { // if (!supportsEnum) {
onSave({ // onSave({
...metaData, // ...metaData,
values: [], // values: [],
restrictDefinedValues: false, // restrictDefinedValues: false,
}); // });
handleHideModal(); // handleHideModal();
return; // return;
} // }
onSave(metaData); onSave(metaData);
handleHideModal(); handleHideModal();
}, [metaData, onSave, handleHideModal, type, valueError]); }, [metaData, onSave, handleHideModal, type, valueError]);
// Handle blur event, synchronize to main state
const handleValueBlur = useCallback(
(values?: string[]) => {
const newValues = values || tempValues;
if (data.values.length > 0) {
newValues.forEach((newValue, index) => {
if (index < data.values.length) {
const originalValue = data.values[index];
if (originalValue !== newValue) {
addUpdateValue(metaData.field, originalValue, newValue);
}
} else {
if (newValue) {
addUpdateValue(metaData.field, '', newValue);
}
}
});
}
handleChange('values', [...new Set([...newValues])]);
},
[handleChange, tempValues, metaData, data, addUpdateValue],
);
// Handle value changes, only update temporary state // Handle value changes, only update temporary state
const handleValueChange = useCallback( const handleValueChange = useCallback(
(index: number, value: string) => { (index: number, value: string, isUpdate: boolean = false) => {
setTempValues((prev) => { setTempValues((prev) => {
if (prev.includes(value)) { if (prev.includes(value)) {
setValueError((prev) => { setValueError((prev) => {
@ -149,32 +168,15 @@ export const useManageValues = (props: IManageValuesProps) => {
} }
const newValues = [...prev]; const newValues = [...prev];
newValues[index] = value; newValues[index] = value;
if (isUpdate) {
handleValueBlur(newValues);
}
return newValues; return newValues;
}); });
}, },
[t, type], [t, type, handleValueBlur],
); );
// Handle blur event, synchronize to main state
const handleValueBlur = useCallback(() => {
if (data.values.length > 0) {
tempValues.forEach((newValue, index) => {
if (index < data.values.length) {
const originalValue = data.values[index];
if (originalValue !== newValue) {
addUpdateValue(metaData.field, originalValue, newValue);
}
} else {
if (newValue) {
addUpdateValue(metaData.field, '', newValue);
}
}
});
}
handleChange('values', [...new Set([...tempValues])]);
}, [handleChange, tempValues, metaData, data, addUpdateValue]);
// Handle delete operation // Handle delete operation
const handleDelete = useCallback( const handleDelete = useCallback(
(index: number) => { (index: number) => {
@ -198,6 +200,14 @@ export const useManageValues = (props: IManageValuesProps) => {
[addDeleteValue, metaData], [addDeleteValue, metaData],
); );
const handleClearValues = useCallback(() => {
setTempValues([]);
setMetaData((prev) => ({
...prev,
values: [],
}));
}, [setTempValues, setMetaData]);
const showDeleteModal = (item: string, callback: () => void) => { const showDeleteModal = (item: string, callback: () => void) => {
setDeleteDialogContent({ setDeleteDialogContent({
visible: true, visible: true,
@ -227,6 +237,7 @@ export const useManageValues = (props: IManageValuesProps) => {
return { return {
metaData, metaData,
handleClearValues,
tempValues, tempValues,
valueError, valueError,
deleteDialogContent, deleteDialogContent,

View File

@ -1,6 +1,10 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { MetadataType } from './hook'; import { MetadataType } from './hooks/use-manage-modal';
export type IMetaDataReturnType = Record<string, Array<Array<string | number>>>; export type IMetaDataReturnType = Record<
string,
| { type: string; values: Array<Array<string | number>> }
| Array<Array<string | number>>
>;
export type IMetaDataReturnJSONType = Record< export type IMetaDataReturnJSONType = Record<
string, string,
Array<string | number> | string Array<string | number> | string
@ -32,11 +36,11 @@ export type IMetaDataReturnJSONSettings =
export type MetadataValueType = export type MetadataValueType =
| 'string' | 'string'
| 'bool' // | 'list<string>'
| 'enum' // | 'bool'
// | 'enum'
| 'time' | 'time'
| 'int' | 'number';
| 'float';
export type IMetaDataTableData = { export type IMetaDataTableData = {
field: string; field: string;
@ -52,6 +56,7 @@ export type IBuiltInMetadataItem = {
}; };
export type IManageModalProps = { export type IManageModalProps = {
documentIds?: string[];
title: ReactNode; title: ReactNode;
isShowDescription?: boolean; isShowDescription?: boolean;
isDeleteSingleValue?: boolean; isDeleteSingleValue?: boolean;
@ -118,4 +123,5 @@ export type ShowManageMetadataModalProps = Partial<IManageModalProps> & {
options?: ShowManageMetadataModalOptions; options?: ShowManageMetadataModalOptions;
title?: ReactNode | string; title?: ReactNode | string;
isDeleteSingleValue?: boolean; isDeleteSingleValue?: boolean;
documentIds?: string[];
}; };

View File

@ -0,0 +1,372 @@
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { ColumnDef } from '@tanstack/react-table';
import {
ListChevronsDownUp,
ListChevronsUpDown,
Settings,
Trash2,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
getMetadataValueTypeLabel,
MetadataDeleteMap,
MetadataType,
} from './hooks/use-manage-modal';
import { IMetaDataTableData } from './interface';
interface IUseMetadataColumns {
isDeleteSingleValue: boolean;
metadataType: MetadataType;
handleDeleteSingleValue: (field: string, value: string) => void;
handleDeleteSingleRow: (field: string) => void;
handleEditValueRow: (data: IMetaDataTableData, index: number) => void;
showTypeColumn: boolean;
isShowDescription: boolean;
setTableData: React.Dispatch<React.SetStateAction<IMetaDataTableData[]>>;
setShouldSave: React.Dispatch<React.SetStateAction<boolean>>;
}
export const useMetadataColumns = ({
isDeleteSingleValue,
metadataType,
handleDeleteSingleValue,
handleDeleteSingleRow,
handleEditValueRow,
isShowDescription,
setTableData,
setShouldSave,
}: IUseMetadataColumns) => {
const { t } = useTranslation();
const [deleteDialogContent, setDeleteDialogContent] = useState({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
const [expanded, setExpanded] = useState(true);
const [editingValue, setEditingValue] = useState<{
field: string;
value: string;
newValue: string;
} | null>(null);
const isSettingsMode =
metadataType === MetadataType.Setting ||
metadataType === MetadataType.SingleFileSetting ||
metadataType === MetadataType.UpdateSingle;
const showTypeColumn = isSettingsMode;
const handleEditValue = (field: string, value: string) => {
setEditingValue({ field, value, newValue: value });
};
const saveEditedValue = useCallback(() => {
if (editingValue) {
setTableData((prev) => {
return prev.map((row) => {
if (row.field === editingValue.field) {
const updatedValues = row.values.map((v) =>
v === editingValue.value ? editingValue.newValue : v,
);
return { ...row, values: updatedValues };
}
return row;
});
});
setEditingValue(null);
setShouldSave(true);
}
}, [editingValue, setTableData]);
const cancelEditValue = () => {
setEditingValue(null);
};
const hideDeleteModal = () => {
setDeleteDialogContent({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
};
const columns = useMemo(() => {
const cols: ColumnDef<IMetaDataTableData>[] = [
...(MetadataType.Manage === metadataType ||
MetadataType.UpdateSingle === metadataType
? [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
]
: []),
{
accessorKey: 'field',
header: () => <span>{t('knowledgeDetails.metadata.field')}</span>,
cell: ({ row }) => (
<div className="text-sm text-accent-primary">
{row.getValue('field')}
</div>
),
},
// ...(showTypeColumn
// ? ([
// {
// accessorKey: 'valueType',
// header: () => <span>Type</span>,
// cell: ({ row }) => (
// <div className="text-sm">
// {getMetadataValueTypeLabel(
// row.original.valueType as IMetaDataTableData['valueType'],
// )}
// </div>
// ),
// },
// ] as ColumnDef<IMetaDataTableData>[])
// : []),
{
accessorKey: 'description',
header: () => <span>{t('knowledgeDetails.metadata.description')}</span>,
cell: ({ row }) => (
<div className="text-sm truncate max-w-32">
{row.getValue('description')}
</div>
),
},
{
accessorKey: 'valueType',
header: () => <span>{t('knowledgeDetails.metadata.type')}</span>,
cell: ({ row }) => (
<div className="text-sm">
{getMetadataValueTypeLabel(
row.original.valueType as IMetaDataTableData['valueType'],
)}
</div>
),
},
{
accessorKey: 'values',
header: () => (
<div className="flex items-center">
<span>{t('knowledgeDetails.metadata.values')}</span>
<div
className="ml-2 p-1 cursor-pointer"
onClick={() => {
setExpanded(!expanded);
}}
>
{expanded ? (
<ListChevronsDownUp size={14} />
) : (
<ListChevronsUpDown size={14} />
)}
{expanded}
</div>
</div>
),
cell: ({ row }) => {
const values = row.getValue('values') as Array<string>;
// const supportsEnum = isMetadataValueTypeWithEnum(
// row.original.valueType,
// );
// if (!supportsEnum || !Array.isArray(values) || values.length === 0) {
// return <div></div>;
// }
const displayedValues = expanded ? values : values.slice(0, 2);
const hasMore = Array.isArray(values) && values.length > 2;
return (
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">
{displayedValues?.map((value: string) => {
const isEditing =
editingValue &&
editingValue.field === row.getValue('field') &&
editingValue.value === value;
return isEditing ? (
<div key={value}>
<Input
type="text"
value={editingValue.newValue}
onChange={(e) =>
setEditingValue({
...editingValue,
newValue: e.target.value,
})
}
onBlur={saveEditedValue}
onKeyDown={(e) => {
if (e.key === 'Enter') {
saveEditedValue();
} else if (e.key === 'Escape') {
cancelEditValue();
}
}}
autoFocus
// className="text-sm min-w-20 max-w-32 outline-none bg-transparent px-1 py-0.5"
/>
</div>
) : (
<Button
key={value}
variant={'ghost'}
className="border border-border-button"
onClick={() =>
handleEditValue(row.getValue('field'), value)
}
aria-label="Edit"
>
<div className="flex gap-1 items-center">
<div className="text-sm truncate max-w-24">{value}</div>
{isDeleteSingleValue && (
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={(e) => {
e.stopPropagation();
// showDeleteDialogContent(value, row);
setDeleteDialogContent({
visible: true,
title:
t('common.delete') +
' ' +
t('knowledgeDetails.metadata.value'),
name: value,
warnText:
MetadataDeleteMap(t)[
metadataType as MetadataType
].warnValueText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleValue(
row.getValue('field'),
value,
);
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
)}
</div>
</Button>
);
})}
{hasMore && !expanded && (
<div className="text-text-secondary self-end">...</div>
)}
</div>
</div>
);
},
},
{
accessorKey: 'action',
header: () => <span>{t('knowledgeDetails.metadata.action')}</span>,
meta: {
cellClassName: 'w-12',
},
cell: ({ row }) => (
<div className=" flex opacity-0 group-hover:opacity-100 gap-2">
<Button
variant={'ghost'}
className="bg-transparent px-1 py-0"
onClick={() => {
handleEditValueRow(row.original, row.index);
}}
>
<Settings />
</Button>
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={() => {
setDeleteDialogContent({
visible: true,
title:
// t('common.delete') +
// ' ' +
// t('knowledgeDetails.metadata.metadata')
MetadataDeleteMap(t)[metadataType as MetadataType].title,
name: row.getValue('field'),
warnText:
MetadataDeleteMap(t)[metadataType as MetadataType]
.warnFieldText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleRow(row.getValue('field'));
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
</div>
),
},
];
if (!isShowDescription) {
return cols.filter((col) => {
if ('accessorKey' in col && col.accessorKey === 'description') {
return false;
}
return true;
});
}
return cols;
}, [
handleDeleteSingleRow,
t,
handleDeleteSingleValue,
isShowDescription,
isDeleteSingleValue,
handleEditValueRow,
metadataType,
expanded,
editingValue,
saveEditedValue,
showTypeColumn,
]);
return {
columns,
deleteDialogContent,
};
};

View File

@ -1,3 +1,4 @@
import { BulkOperateBar } from '@/components/bulk-operate-bar';
import { import {
ConfirmDeleteDialog, ConfirmDeleteDialog,
ConfirmDeleteDialogNode, ConfirmDeleteDialogNode,
@ -5,7 +6,6 @@ import {
import { EmptyType } from '@/components/empty/constant'; import { EmptyType } from '@/components/empty/constant';
import Empty from '@/components/empty/empty'; import Empty from '@/components/empty/empty';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal'; import { Modal } from '@/components/ui/modal/modal';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { import {
@ -18,9 +18,9 @@ import {
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { import {
ColumnDef,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
@ -28,28 +28,22 @@ import {
getSortedRowModel, getSortedRowModel,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { import { Plus, Trash2 } from 'lucide-react';
ListChevronsDownUp,
ListChevronsUpDown,
Plus,
Settings,
Trash2,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from '../../sidebar/hooks'; import { useHandleMenuClick } from '../../sidebar/hooks';
import { import {
MetadataDeleteMap,
MetadataType,
getMetadataValueTypeLabel, getMetadataValueTypeLabel,
isMetadataValueTypeWithEnum, MetadataType,
useManageMetaDataModal, useManageMetaDataModal,
useOperateData,
} from './hooks/use-manage-modal'; } from './hooks/use-manage-modal';
import { import {
IBuiltInMetadataItem, IBuiltInMetadataItem,
IManageModalProps, IManageModalProps,
IMetaDataTableData, IMetaDataTableData,
} from './interface'; } from './interface';
import { useMetadataColumns } from './manage-modal-column';
import { ManageValuesModal } from './manage-values-modal'; import { ManageValuesModal } from './manage-values-modal';
type MetadataSettingsTab = 'generation' | 'built-in'; type MetadataSettingsTab = 'generation' | 'built-in';
@ -71,6 +65,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
isVerticalShowValue = true, isVerticalShowValue = true,
builtInMetadata, builtInMetadata,
success, success,
documentIds,
} = props; } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [valueData, setValueData] = useState<IMetaDataTableData>({ const [valueData, setValueData] = useState<IMetaDataTableData>({
@ -80,25 +75,11 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
valueType: 'string', valueType: 'string',
}); });
const [expanded, setExpanded] = useState(true);
const [activeTab, setActiveTab] = useState<MetadataSettingsTab>('generation'); const [activeTab, setActiveTab] = useState<MetadataSettingsTab>('generation');
const [currentValueIndex, setCurrentValueIndex] = useState<number>(0); const [currentValueIndex, setCurrentValueIndex] = useState<number>(0);
const [builtInSelection, setBuiltInSelection] = useState< const [builtInSelection, setBuiltInSelection] = useState<
IBuiltInMetadataItem[] IBuiltInMetadataItem[]
>([]); >([]);
const [deleteDialogContent, setDeleteDialogContent] = useState({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
const [editingValue, setEditingValue] = useState<{
field: string;
value: string;
newValue: string;
} | null>(null);
const { const {
tableData, tableData,
@ -108,7 +89,13 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
handleSave, handleSave,
addUpdateValue, addUpdateValue,
addDeleteValue, addDeleteValue,
} = useManageMetaDataModal(originalTableData, metadataType, otherData); handleDeleteBatchRow,
} = useManageMetaDataModal(
originalTableData,
metadataType,
otherData,
documentIds,
);
const { handleMenuClick } = useHandleMenuClick(); const { handleMenuClick } = useHandleMenuClick();
const [shouldSave, setShouldSave] = useState(false); const [shouldSave, setShouldSave] = useState(false);
const { const {
@ -116,20 +103,12 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
showModal: showManageValuesModal, showModal: showManageValuesModal,
hideModal: hideManageValuesModal, hideModal: hideManageValuesModal,
} = useSetModalState(); } = useSetModalState();
const hideDeleteModal = () => {
setDeleteDialogContent({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
};
const isSettingsMode = const isSettingsMode =
metadataType === MetadataType.Setting || metadataType === MetadataType.Setting ||
metadataType === MetadataType.SingleFileSetting; metadataType === MetadataType.SingleFileSetting ||
metadataType === MetadataType.UpdateSingle;
const showTypeColumn = isSettingsMode; const showTypeColumn = isSettingsMode;
const builtInRows = useMemo( const builtInRows = useMemo(
() => [ () => [
@ -183,31 +162,6 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
[builtInSelection], [builtInSelection],
); );
const handleEditValue = (field: string, value: string) => {
setEditingValue({ field, value, newValue: value });
};
const saveEditedValue = useCallback(() => {
if (editingValue) {
setTableData((prev) => {
return prev.map((row) => {
if (row.field === editingValue.field) {
const updatedValues = row.values.map((v) =>
v === editingValue.value ? editingValue.newValue : v,
);
return { ...row, values: updatedValues };
}
return row;
});
});
setEditingValue(null);
setShouldSave(true);
}
}, [editingValue, setTableData]);
const cancelEditValue = () => {
setEditingValue(null);
};
const handAddValueRow = () => { const handAddValueRow = () => {
setValueData({ setValueData({
field: '', field: '',
@ -226,229 +180,19 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
}, },
[showManageValuesModal], [showManageValuesModal],
); );
const { rowSelection, rowSelectionIsEmpty, setRowSelection, selectedCount } =
const columns: ColumnDef<IMetaDataTableData>[] = useMemo(() => { useRowSelection();
const cols: ColumnDef<IMetaDataTableData>[] = [ const { columns, deleteDialogContent } = useMetadataColumns({
{ isDeleteSingleValue: !!isDeleteSingleValue,
accessorKey: 'field',
header: () => <span>{t('knowledgeDetails.metadata.field')}</span>,
cell: ({ row }) => (
<div className="text-sm text-accent-primary">
{row.getValue('field')}
</div>
),
},
...(showTypeColumn
? ([
{
accessorKey: 'valueType',
header: () => <span>Type</span>,
cell: ({ row }) => (
<div className="text-sm">
{getMetadataValueTypeLabel(
row.original.valueType as IMetaDataTableData['valueType'],
)}
</div>
),
},
] as ColumnDef<IMetaDataTableData>[])
: []),
{
accessorKey: 'description',
header: () => <span>{t('knowledgeDetails.metadata.description')}</span>,
cell: ({ row }) => (
<div className="text-sm truncate max-w-32">
{row.getValue('description')}
</div>
),
},
{
accessorKey: 'values',
header: () => (
<div className="flex items-center">
<span>{t('knowledgeDetails.metadata.values')}</span>
<div
className="ml-2 p-1 cursor-pointer"
onClick={() => {
setExpanded(!expanded);
}}
>
{expanded ? (
<ListChevronsDownUp size={14} />
) : (
<ListChevronsUpDown size={14} />
)}
{expanded}
</div>
</div>
),
cell: ({ row }) => {
const values = row.getValue('values') as Array<string>;
const supportsEnum = isMetadataValueTypeWithEnum(
row.original.valueType,
);
if (!supportsEnum || !Array.isArray(values) || values.length === 0) {
return <div></div>;
}
const displayedValues = expanded ? values : values.slice(0, 2);
const hasMore = Array.isArray(values) && values.length > 2;
return (
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">
{displayedValues?.map((value: string) => {
const isEditing =
editingValue &&
editingValue.field === row.getValue('field') &&
editingValue.value === value;
return isEditing ? (
<div key={value}>
<Input
type="text"
value={editingValue.newValue}
onChange={(e) =>
setEditingValue({
...editingValue,
newValue: e.target.value,
})
}
onBlur={saveEditedValue}
onKeyDown={(e) => {
if (e.key === 'Enter') {
saveEditedValue();
} else if (e.key === 'Escape') {
cancelEditValue();
}
}}
autoFocus
// className="text-sm min-w-20 max-w-32 outline-none bg-transparent px-1 py-0.5"
/>
</div>
) : (
<Button
key={value}
variant={'ghost'}
className="border border-border-button"
onClick={() =>
handleEditValue(row.getValue('field'), value)
}
aria-label="Edit"
>
<div className="flex gap-1 items-center">
<div className="text-sm truncate max-w-24">{value}</div>
{isDeleteSingleValue && (
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={(e) => {
e.stopPropagation();
setDeleteDialogContent({
visible: true,
title:
t('common.delete') +
' ' +
t('knowledgeDetails.metadata.value'),
name: value,
warnText:
MetadataDeleteMap(t)[
metadataType as MetadataType
].warnValueText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleValue(
row.getValue('field'),
value,
);
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
)}
</div>
</Button>
);
})}
{hasMore && !expanded && (
<div className="text-text-secondary self-end">...</div>
)}
</div>
</div>
);
},
},
{
accessorKey: 'action',
header: () => <span>{t('knowledgeDetails.metadata.action')}</span>,
meta: {
cellClassName: 'w-12',
},
cell: ({ row }) => (
<div className=" flex opacity-0 group-hover:opacity-100 gap-2">
<Button
variant={'ghost'}
className="bg-transparent px-1 py-0"
onClick={() => {
handleEditValueRow(row.original, row.index);
}}
>
<Settings />
</Button>
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={() => {
setDeleteDialogContent({
visible: true,
title:
// t('common.delete') +
// ' ' +
// t('knowledgeDetails.metadata.metadata')
MetadataDeleteMap(t)[metadataType as MetadataType].title,
name: row.getValue('field'),
warnText:
MetadataDeleteMap(t)[metadataType as MetadataType]
.warnFieldText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleRow(row.getValue('field'));
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
</div>
),
},
];
if (!isShowDescription) {
return cols.filter((col) => col.accessorKey !== 'description');
}
return cols;
}, [
handleDeleteSingleRow,
t,
handleDeleteSingleValue,
isShowDescription,
isDeleteSingleValue,
handleEditValueRow,
metadataType, metadataType,
expanded, setTableData,
editingValue, handleDeleteSingleValue,
saveEditedValue, handleDeleteSingleRow,
handleEditValueRow,
isShowDescription,
showTypeColumn, showTypeColumn,
]); setShouldSave,
});
const table = useReactTable({ const table = useReactTable({
data: tableData as IMetaDataTableData[], data: tableData as IMetaDataTableData[],
@ -457,7 +201,11 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection,
manualPagination: true, manualPagination: true,
state: {
rowSelection,
},
}); });
const handleSaveValues = (data: IMetaDataTableData) => { const handleSaveValues = (data: IMetaDataTableData) => {
@ -506,7 +254,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
handleSave({ callback: () => {}, builtInMetadata: builtInSelection }); handleSave({ callback: () => {}, builtInMetadata: builtInSelection });
setShouldSave(false); setShouldSave(false);
}, 0); }, 0);
console.log('shouldSave');
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [tableData, shouldSave, handleSave, builtInSelection]); }, [tableData, shouldSave, handleSave, builtInSelection]);
@ -515,6 +263,25 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
return tableData.map((item) => item.field); return tableData.map((item) => item.field);
}, [tableData]); }, [tableData]);
const { handleDelete } = useOperateData({
rowSelection,
list: tableData,
handleDeleteBatchRow,
});
const operateList = [
{
id: 'delete',
label: t('common.delete'),
icon: <Trash2 />,
onClick: async () => {
await handleDelete();
// if (code === 0) {
// setRowSelection({});
// }
},
},
];
return ( return (
<> <>
<Modal <Modal
@ -528,7 +295,6 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
callback: hideModal, callback: hideModal,
builtInMetadata: builtInSelection, builtInMetadata: builtInSelection,
}); });
console.log('data', res);
success?.(res); success?.(res);
}} }}
> >
@ -559,15 +325,25 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
</Button> </Button>
)} )}
</div> </div>
{metadataType === MetadataType.Setting ? (
{rowSelectionIsEmpty || (
<BulkOperateBar
list={operateList}
count={selectedCount}
></BulkOperateBar>
)}
{metadataType === MetadataType.Setting ||
metadataType === MetadataType.SingleFileSetting ? (
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={(v) => setActiveTab(v as MetadataSettingsTab)} onValueChange={(v) => setActiveTab(v as MetadataSettingsTab)}
> >
<TabsList className="w-fit"> <TabsList className="w-fit">
<TabsTrigger value="generation">Generation</TabsTrigger> <TabsTrigger value="generation">
{t('knowledgeDetails.metadata.generation')}
</TabsTrigger>
<TabsTrigger value="built-in"> <TabsTrigger value="built-in">
{t('knowledgeConfiguration.builtIn')} {t('knowledgeDetails.metadata.builtIn')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="generation"> <TabsContent value="generation">

View File

@ -2,49 +2,87 @@ import {
ConfirmDeleteDialog, ConfirmDeleteDialog,
ConfirmDeleteDialogNode, ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog'; } from '@/components/confirm-delete-dialog';
import { DynamicForm, FormFieldType } from '@/components/dynamic-form';
import EditTag from '@/components/edit-tag'; import EditTag from '@/components/edit-tag';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { FormLabel } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { DateInput } from '@/components/ui/input-date';
import { Modal } from '@/components/ui/modal/modal'; import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowSelect } from '@/components/ui/select'; import { formatPureDate } from '@/utils/date';
import { Textarea } from '@/components/ui/textarea'; import dayjs from 'dayjs';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import { memo } from 'react'; import { memo, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { metadataValueTypeOptions } from './hooks/use-manage-modal';
isMetadataValueTypeWithEnum,
metadataValueTypeOptions,
} from './hooks/use-manage-modal';
import { useManageValues } from './hooks/use-manage-values-modal'; import { useManageValues } from './hooks/use-manage-values-modal';
import { IManageValuesProps } from './interface'; import { IManageValuesProps, MetadataValueType } from './interface';
// Create a separate input component, wrapped with memo to avoid unnecessary re-renders // Create a separate input component, wrapped with memo to avoid unnecessary re-renders
const ValueInputItem = memo( const ValueInputItem = memo(
({ ({
item, item,
index, index,
type,
onValueChange, onValueChange,
onDelete, onDelete,
onBlur, onBlur,
}: { }: {
item: string; item: string;
index: number; index: number;
onValueChange: (index: number, value: string) => void; type: MetadataValueType;
onValueChange: (index: number, value: string, isUpdate?: boolean) => void;
onDelete: (index: number) => void; onDelete: (index: number) => void;
onBlur: (index: number) => void; onBlur: (index: number) => void;
}) => { }) => {
const value = useMemo(() => {
if (type === 'time') {
if (item) {
try {
// Using dayjs to parse date strings in various formats including DD/MM/YYYY
const parsedDate = dayjs(item, [
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'DD-MM-YYYY',
'MM-DD-YYYY',
]);
if (!parsedDate.isValid()) {
console.error('Invalid date format:', item);
return undefined; // Return current date as fallback
}
return parsedDate.toDate();
} catch (error) {
console.error('Error parsing date:', item, error);
return undefined; // Return current date as fallback
}
}
return undefined;
}
return item;
}, [item, type]);
return ( return (
<div <div
key={`value-item-${index}`} key={`value-item-${index}`}
className="flex items-center gap-2.5 w-full" className="flex items-center gap-2.5 w-full"
> >
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<Input {type === 'time' && (
value={item} <DateInput
onChange={(e) => onValueChange(index, e.target.value)} value={value as Date}
onBlur={() => onBlur(index)} onChange={(value) => {
/> onValueChange(index, formatPureDate(value), true);
}}
/>
)}
{type !== 'time' && (
<Input
value={value as string}
type={type === 'number' ? 'number' : 'text'}
onChange={(e) => onValueChange(index, e.target.value)}
onBlur={() => onBlur(index)}
/>
)}
</div> </div>
<Button <Button
type="button" type="button"
@ -59,12 +97,15 @@ const ValueInputItem = memo(
}, },
); );
ValueInputItem.displayName = 'ValueInputItem';
export const ManageValuesModal = (props: IManageValuesProps) => { export const ManageValuesModal = (props: IManageValuesProps) => {
const { const {
title, title,
isEditField, isEditField,
visible, visible,
isAddValue, isAddValue,
isShowValueSwitch,
isShowDescription, isShowDescription,
isVerticalShowValue, isVerticalShowValue,
isShowType, isShowType,
@ -74,6 +115,7 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
tempValues, tempValues,
valueError, valueError,
deleteDialogContent, deleteDialogContent,
handleClearValues,
handleChange, handleChange,
handleValueChange, handleValueChange,
handleValueBlur, handleValueBlur,
@ -84,7 +126,79 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
handleHideModal, handleHideModal,
} = useManageValues(props); } = useManageValues(props);
const { t } = useTranslation(); const { t } = useTranslation();
const canShowValues = isMetadataValueTypeWithEnum(metaData.valueType);
const formRef = useRef<any>();
const [valueType, setValueType] = useState<MetadataValueType>(
metaData.valueType || 'string',
);
// Define form fields based on component properties
const formFields = [
...(isEditField
? [
{
name: 'field',
label: t('knowledgeDetails.metadata.fieldName'),
type: FormFieldType.Text,
required: true,
validation: {
pattern: /^[a-zA-Z_]*$/,
message: t('knowledgeDetails.metadata.fieldNameInvalid'),
},
defaultValue: metaData.field,
onChange: (value: string) => handleChange('field', value),
},
]
: []),
...(isShowType
? [
{
name: 'valueType',
label: 'Type',
type: FormFieldType.Select,
options: metadataValueTypeOptions,
defaultValue: metaData.valueType || 'string',
onChange: (value: string) => {
setValueType(value as MetadataValueType);
handleChange('valueType', value);
handleClearValues();
},
},
]
: []),
...(isShowDescription
? [
{
name: 'description',
label: t('knowledgeDetails.metadata.description'),
type: FormFieldType.Textarea,
tooltip: t('knowledgeDetails.metadata.descriptionTip'),
defaultValue: metaData.description,
className: 'mt-2',
onChange: (value: string) => handleChange('description', value),
},
]
: []),
...(isShowValueSwitch
? [
{
name: 'restrictDefinedValues',
label: t('knowledgeDetails.metadata.restrictDefinedValues'),
tooltip: t('knowledgeDetails.metadata.restrictDefinedValuesTip'),
type: FormFieldType.Switch,
defaultValue: metaData.restrictDefinedValues || false,
onChange: (value: boolean) =>
handleChange('restrictDefinedValues', value),
},
]
: []),
];
// Handle form submission
const handleSubmit = () => {
handleSave();
};
return ( return (
<Modal <Modal
@ -93,7 +207,7 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
onCancel={handleHideModal} onCancel={handleHideModal}
className="!w-[460px]" className="!w-[460px]"
okText={t('common.confirm')} okText={t('common.confirm')}
onOk={handleSave} onOk={() => formRef.current?.submit(handleSubmit)}
maskClosable={false} maskClosable={false}
footer={null} footer={null}
> >
@ -103,52 +217,18 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
{metaData.field} {metaData.field}
</div> </div>
)} )}
{isEditField && (
<div className="flex flex-col gap-2"> {formFields.length > 0 && (
<div>{t('knowledgeDetails.metadata.fieldName')}</div> <DynamicForm.Root
<div> ref={formRef}
<Input fields={formFields}
value={metaData.field} onSubmit={handleSubmit}
onChange={(e) => { className="space-y-4"
const value = e.target?.value || ''; />
if (/^[a-zA-Z_]*$/.test(value)) {
handleChange('field', value);
}
}}
/>
<div className="text-state-error text-sm">{valueError.field}</div>
</div>
</div>
)} )}
{isShowType && (
<div className="flex flex-col gap-2"> {((metaData.restrictDefinedValues && isShowValueSwitch) ||
<div>Type</div> !isShowValueSwitch) && (
<RAGFlowSelect
value={metaData.valueType || 'string'}
options={metadataValueTypeOptions}
onChange={(value) => handleChange('valueType', value)}
/>
</div>
)}
{isShowDescription && (
<div className="flex flex-col gap-2">
<FormLabel
className="text-text-primary text-base"
tooltip={t('knowledgeDetails.metadata.descriptionTip')}
>
{t('knowledgeDetails.metadata.description')}
</FormLabel>
<div>
<Textarea
value={metaData.description}
onChange={(e) => {
handleChange('description', e.target?.value || '');
}}
/>
</div>
</div>
)}
{canShowValues && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div>{t('knowledgeDetails.metadata.values')}</div> <div>{t('knowledgeDetails.metadata.values')}</div>
@ -172,6 +252,7 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
key={`value-item-${index}`} key={`value-item-${index}`}
item={item} item={item}
index={index} index={index}
type={valueType || 'string'}
onValueChange={handleValueChange} onValueChange={handleValueChange}
onDelete={(idx: number) => { onDelete={(idx: number) => {
showDeleteModal(item, () => { showDeleteModal(item, () => {

View File

@ -1,4 +1,7 @@
import { BulkOperateBar } from '@/components/bulk-operate-bar'; import {
BulkOperateBar,
BulkOperateItemType,
} from '@/components/bulk-operate-bar';
import { FileUploadDialog } from '@/components/file-upload-dialog'; import { FileUploadDialog } from '@/components/file-upload-dialog';
import ListFilterBar from '@/components/list-filter-bar'; import ListFilterBar from '@/components/list-filter-bar';
import { RenameDialog } from '@/components/rename-dialog'; import { RenameDialog } from '@/components/rename-dialog';
@ -13,7 +16,7 @@ import {
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection'; import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
import { useFetchDocumentList } from '@/hooks/use-document-request'; import { useFetchDocumentList } from '@/hooks/use-document-request';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { Pen, Upload } from 'lucide-react'; import { Upload } from 'lucide-react';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -95,6 +98,38 @@ export default function Dataset() {
rowSelection, rowSelection,
setRowSelection, setRowSelection,
}); });
const handleAddMetadataWithDocuments = () => {
showManageMetadataModal({
type: MetadataType.Manage,
isCanAdd: false,
isEditField: false,
isDeleteSingleValue: true,
isAddValue: true,
title: (
<div className="flex flex-col gap-2">
<div className="text-base font-normal">
{t('knowledgeDetails.metadata.manageMetadata')}
</div>
<div className="text-sm text-text-secondary">
{t('knowledgeDetails.metadata.manageMetadataForDataset')}
</div>
</div>
),
documentIds: documents.map((doc) => doc.id),
});
};
const updatedList = list.map((item) => {
if (item.id === 'batch-metadata') {
return {
...item,
onClick: handleAddMetadataWithDocuments,
};
}
return item;
});
return ( return (
<> <>
<div className="absolute top-4 right-5"> <div className="absolute top-4 right-5">
@ -118,37 +153,37 @@ export default function Dataset() {
</div> </div>
</div> </div>
} }
preChildren={ // preChildren={
<Button // <Button
variant={'ghost'} // variant={'ghost'}
className="border border-border-button" // className="border border-border-button"
onClick={() => // onClick={() =>
showManageMetadataModal({ // showManageMetadataModal({
type: MetadataType.Manage, // type: MetadataType.Manage,
isCanAdd: false, // isCanAdd: false,
isEditField: true, // isEditField: false,
isDeleteSingleValue: true, // isDeleteSingleValue: true,
title: ( // title: (
<div className="flex flex-col gap-2"> // <div className="flex flex-col gap-2">
<div className="text-base font-normal"> // <div className="text-base font-normal">
{t('knowledgeDetails.metadata.manageMetadata')} // {t('knowledgeDetails.metadata.manageMetadata')}
</div> // </div>
<div className="text-sm text-text-secondary"> // <div className="text-sm text-text-secondary">
{t( // {t(
'knowledgeDetails.metadata.manageMetadataForDataset', // 'knowledgeDetails.metadata.manageMetadataForDataset',
)} // )}
</div> // </div>
</div> // </div>
), // ),
}) // })
} // }
> // >
<div className="flex gap-1 items-center"> // <div className="flex gap-1 items-center">
<Pen size={14} /> // <Pen size={14} />
{t('knowledgeDetails.metadata.metadata')} // {t('knowledgeDetails.metadata.metadata')}
</div> // </div>
</Button> // </Button>
} // }
> >
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -169,7 +204,10 @@ export default function Dataset() {
</DropdownMenu> </DropdownMenu>
</ListFilterBar> </ListFilterBar>
{rowSelectionIsEmpty || ( {rowSelectionIsEmpty || (
<BulkOperateBar list={list} count={selectedCount}></BulkOperateBar> <BulkOperateBar
list={updatedList as BulkOperateItemType[]}
count={selectedCount}
></BulkOperateBar>
)} )}
<DatasetTable <DatasetTable
documents={documents} documents={documents}
@ -218,6 +256,7 @@ export default function Dataset() {
isEditField={metadataConfig.isEditField} isEditField={metadataConfig.isEditField}
isDeleteSingleValue={metadataConfig.isDeleteSingleValue} isDeleteSingleValue={metadataConfig.isDeleteSingleValue}
type={metadataConfig.type} type={metadataConfig.type}
documentIds={metadataConfig.documentIds}
otherData={metadataConfig.record} otherData={metadataConfig.record}
/> />
)} )}

View File

@ -9,7 +9,7 @@ import {
useSetDocumentStatus, useSetDocumentStatus,
} from '@/hooks/use-document-request'; } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
import { Ban, CircleCheck, CircleX, Play, Trash2 } from 'lucide-react'; import { Ban, CircleCheck, CircleX, PenIcon, Play, Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -140,6 +140,11 @@ export function useBulkOperateDataset({
} }
}, },
}, },
{
id: 'batch-metadata',
label: t('knowledgeDetails.metadata.metadata'),
icon: <PenIcon />,
},
]; ];
return { chunkNum, list, visible, hideModal, showModal, handleRunClick }; return { chunkNum, list, visible, hideModal, showModal, handleRunClick };

View File

@ -16,10 +16,7 @@ import { formatDate } from '@/utils/date';
import { ColumnDef } from '@tanstack/table-core'; import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown, MonitorUp } from 'lucide-react'; import { ArrowUpDown, MonitorUp } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { MetadataType } from '../components/metedata/hooks/use-manage-modal';
MetadataType,
util,
} from '../components/metedata/hooks/use-manage-modal';
import { ShowManageMetadataModalProps } from '../components/metedata/interface'; import { ShowManageMetadataModalProps } from '../components/metedata/interface';
import { DatasetActionCell } from './dataset-action-cell'; import { DatasetActionCell } from './dataset-action-cell';
import { ParsingStatusCell } from './parsing-status-cell'; import { ParsingStatusCell } from './parsing-status-cell';
@ -181,12 +178,12 @@ export function useDatasetTableColumns({
className="capitalize cursor-pointer" className="capitalize cursor-pointer"
onClick={() => { onClick={() => {
showManageMetadataModal({ showManageMetadataModal({
metadata: util.JSONToMetaDataTableData( // metadata: util.JSONToMetaDataTableData(
row.original.meta_fields || {}, // row.original.meta_fields || {},
), // ),
isCanAdd: true, isCanAdd: true,
type: MetadataType.UpdateSingle, type: MetadataType.UpdateSingle,
record: row, record: row.original,
title: ( title: (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="text-base font-normal"> <div className="text-base font-normal">
@ -199,6 +196,7 @@ export function useDatasetTableColumns({
</div> </div>
), ),
isDeleteSingleValue: true, isDeleteSingleValue: true,
documentIds: [row.original.id],
}); });
}} }}
> >

View File

@ -264,10 +264,22 @@ export const listDocument = (
export const documentFilter = (kb_id: string) => export const documentFilter = (kb_id: string) =>
request.post(api.get_dataset_filter, { kb_id }); request.post(api.get_dataset_filter, { kb_id });
export const getMetaDataService = ({ kb_id }: { kb_id: string }) => export const getMetaDataService = ({
request.post(api.getMetaData, { data: { kb_id } }); kb_id,
export const updateMetaData = ({ kb_id, data }: { kb_id: string; data: any }) => doc_ids,
request.post(api.updateMetaData, { data: { kb_id, ...data } }); }: {
kb_id: string;
doc_ids?: string[];
}) => request.post(api.getMetaData, { data: { kb_id, doc_ids } });
export const updateMetaData = ({
kb_id,
doc_ids,
data,
}: {
kb_id: string;
doc_ids?: string[];
data: any;
}) => request.post(api.updateMetaData, { data: { kb_id, doc_ids, ...data } });
export const listDataPipelineLogDocument = ( export const listDataPipelineLogDocument = (
params?: IFetchKnowledgeListRequestParams, params?: IFetchKnowledgeListRequestParams,