mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 17:16:52 +08:00
Feature: Implement metadata functionality (#12049)
### What problem does this PR solve? Feature: Implement metadata functionality ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
370
web/src/pages/dataset/components/metedata/hook.ts
Normal file
370
web/src/pages/dataset/components/metedata/hook.ts
Normal file
@ -0,0 +1,370 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useSetDocumentMeta } from '@/hooks/use-document-request';
|
||||
import {
|
||||
getMetaDataService,
|
||||
updateMetaData,
|
||||
} from '@/services/knowledge-service';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import {
|
||||
IMetaDataReturnJSONSettings,
|
||||
IMetaDataReturnJSONType,
|
||||
IMetaDataReturnType,
|
||||
IMetaDataTableData,
|
||||
MetadataOperations,
|
||||
ShowManageMetadataModalProps,
|
||||
} from './interface';
|
||||
export enum MetadataType {
|
||||
Manage = 1,
|
||||
UpdateSingle = 2,
|
||||
Setting = 3,
|
||||
}
|
||||
export const util = {
|
||||
changeToMetaDataTableData(data: IMetaDataReturnType): IMetaDataTableData[] {
|
||||
return Object.entries(data).map(([key, value]) => {
|
||||
const values = value.map(([v]) => v.toString());
|
||||
console.log('values', values);
|
||||
return {
|
||||
field: key,
|
||||
description: '',
|
||||
values: values,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
|
||||
JSONToMetaDataTableData(
|
||||
data: Record<string, string | string[]>,
|
||||
): IMetaDataTableData[] {
|
||||
return Object.entries(data).map(([key, value]) => {
|
||||
return {
|
||||
field: key,
|
||||
description: '',
|
||||
values: value,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
|
||||
tableDataToMetaDataJSON(data: IMetaDataTableData[]): IMetaDataReturnJSONType {
|
||||
return data.reduce<IMetaDataReturnJSONType>((pre, cur) => {
|
||||
pre[cur.field] = cur.values;
|
||||
return pre;
|
||||
}, {});
|
||||
},
|
||||
|
||||
tableDataToMetaDataSettingJSON(
|
||||
data: IMetaDataTableData[],
|
||||
): IMetaDataReturnJSONSettings {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
key: item.field,
|
||||
description: item.description,
|
||||
enum: item.values,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
metaDataSettingJSONToMetaDataTableData(
|
||||
data: IMetaDataReturnJSONSettings,
|
||||
): IMetaDataTableData[] {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
field: item.key,
|
||||
description: item.description,
|
||||
values: item.enum,
|
||||
restrictDefinedValues: !!item.enum?.length,
|
||||
} as IMetaDataTableData;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const useMetadataOperations = () => {
|
||||
const [operations, setOperations] = useState<MetadataOperations>({
|
||||
deletes: [],
|
||||
updates: [],
|
||||
});
|
||||
|
||||
const addDeleteRow = useCallback((key: string) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
deletes: [...prev.deletes, { key }],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const addDeleteValue = useCallback((key: string, value: string) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
deletes: [...prev.deletes, { key, value }],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const addUpdateValue = useCallback(
|
||||
(key: string, value: string | string[]) => {
|
||||
setOperations((prev) => ({
|
||||
...prev,
|
||||
updates: [...prev.updates, { key, value }],
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetOperations = useCallback(() => {
|
||||
setOperations({
|
||||
deletes: [],
|
||||
updates: [],
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
operations,
|
||||
addDeleteRow,
|
||||
addDeleteValue,
|
||||
addUpdateValue,
|
||||
resetOperations,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFetchMetaDataManageData = (
|
||||
type: MetadataType = MetadataType.Manage,
|
||||
) => {
|
||||
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 {
|
||||
data,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useQuery<IMetaDataTableData[]>({
|
||||
queryKey: ['fetchMetaData', id],
|
||||
enabled: !!id && type === MetadataType.Manage,
|
||||
initialData: [],
|
||||
gcTime: 1000,
|
||||
queryFn: async () => {
|
||||
const { data } = await getMetaDataService({
|
||||
kb_id: id as string,
|
||||
});
|
||||
if (data?.data?.summary) {
|
||||
return util.changeToMetaDataTableData(data.data.summary);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageMetaDataModal = (
|
||||
metaData: IMetaDataTableData[] = [],
|
||||
type: MetadataType = MetadataType.Manage,
|
||||
otherData?: Record<string, any>,
|
||||
) => {
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { data, loading } = useFetchMetaDataManageData(type);
|
||||
|
||||
const [tableData, setTableData] = useState<IMetaDataTableData[]>(metaData);
|
||||
|
||||
const { operations, addDeleteRow, addDeleteValue, addUpdateValue } =
|
||||
useMetadataOperations();
|
||||
|
||||
const { setDocumentMeta } = useSetDocumentMeta();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setTableData(data);
|
||||
} else {
|
||||
setTableData([]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (metaData) {
|
||||
setTableData(metaData);
|
||||
} else {
|
||||
setTableData([]);
|
||||
}
|
||||
}, [metaData]);
|
||||
|
||||
const handleDeleteSingleValue = useCallback(
|
||||
(field: string, value: string) => {
|
||||
addDeleteValue(field, value);
|
||||
|
||||
setTableData((prevTableData) => {
|
||||
const newTableData = prevTableData.map((item) => {
|
||||
if (item.field === field) {
|
||||
return {
|
||||
...item,
|
||||
values: item.values.filter((v) => v !== value),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
// console.log('newTableData', newTableData, prevTableData);
|
||||
return newTableData;
|
||||
});
|
||||
},
|
||||
[addDeleteValue],
|
||||
);
|
||||
|
||||
const handleDeleteSingleRow = useCallback(
|
||||
(field: string) => {
|
||||
addDeleteRow(field);
|
||||
setTableData((prevTableData) => {
|
||||
const newTableData = prevTableData.filter(
|
||||
(item) => item.field !== field,
|
||||
);
|
||||
// console.log('newTableData', newTableData, prevTableData);
|
||||
return newTableData;
|
||||
});
|
||||
},
|
||||
[addDeleteRow],
|
||||
);
|
||||
|
||||
const handleSaveManage = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const { data: res } = await updateMetaData({
|
||||
kb_id: id as string,
|
||||
data: operations,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success(t('message.success'));
|
||||
callback();
|
||||
}
|
||||
},
|
||||
[operations, id, t],
|
||||
);
|
||||
|
||||
const handleSaveUpdateSingle = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const reqData = util.tableDataToMetaDataJSON(tableData);
|
||||
if (otherData?.id) {
|
||||
const ret = await setDocumentMeta({
|
||||
documentId: otherData?.id,
|
||||
meta: JSON.stringify(reqData),
|
||||
});
|
||||
if (ret === 0) {
|
||||
// message.success(t('message.success'));
|
||||
callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
[tableData, otherData, setDocumentMeta],
|
||||
);
|
||||
|
||||
const handleSaveSettings = useCallback(
|
||||
async (callback: () => void) => {
|
||||
const data = util.tableDataToMetaDataSettingJSON(tableData);
|
||||
callback();
|
||||
|
||||
return data;
|
||||
},
|
||||
[tableData],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async ({ callback }: { callback: () => void }) => {
|
||||
switch (type) {
|
||||
case MetadataType.UpdateSingle:
|
||||
handleSaveUpdateSingle(callback);
|
||||
break;
|
||||
case MetadataType.Manage:
|
||||
handleSaveManage(callback);
|
||||
break;
|
||||
case MetadataType.Setting:
|
||||
return handleSaveSettings(callback);
|
||||
default:
|
||||
handleSaveManage(callback);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[handleSaveManage, type, handleSaveUpdateSingle, handleSaveSettings],
|
||||
);
|
||||
|
||||
return {
|
||||
tableData,
|
||||
setTableData,
|
||||
handleDeleteSingleValue,
|
||||
handleDeleteSingleRow,
|
||||
loading,
|
||||
handleSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageMetadata = () => {
|
||||
const [tableData, setTableData] = useState<IMetaDataTableData[]>([]);
|
||||
const [config, setConfig] = useState<ShowManageMetadataModalProps>(
|
||||
{} as ShowManageMetadataModalProps,
|
||||
);
|
||||
const {
|
||||
visible: manageMetadataVisible,
|
||||
showModal,
|
||||
hideModal: hideManageMetadataModal,
|
||||
} = useSetModalState();
|
||||
const showManageMetadataModal = useCallback(
|
||||
(config?: ShowManageMetadataModalProps) => {
|
||||
const { metadata } = config || {};
|
||||
if (metadata) {
|
||||
// const dataTemp = Object.entries(metadata).map(([key, value]) => {
|
||||
// return {
|
||||
// field: key,
|
||||
// description: '',
|
||||
// values: Array.isArray(value) ? value : [value],
|
||||
// } as IMetaDataTableData;
|
||||
// });
|
||||
setTableData(metadata);
|
||||
console.log('metadata-2', metadata);
|
||||
}
|
||||
console.log('metadata-3', metadata);
|
||||
if (config) {
|
||||
setConfig(config);
|
||||
}
|
||||
showModal();
|
||||
},
|
||||
[showModal],
|
||||
);
|
||||
return {
|
||||
manageMetadataVisible,
|
||||
showManageMetadataModal,
|
||||
hideManageMetadataModal,
|
||||
tableData,
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
||||
export const useManageValues = () => {
|
||||
const [updateValues, setUpdateValues] = useState<{
|
||||
field: string;
|
||||
values: string[];
|
||||
} | null>(null);
|
||||
return { updateValues, setUpdateValues };
|
||||
};
|
||||
80
web/src/pages/dataset/components/metedata/interface.ts
Normal file
80
web/src/pages/dataset/components/metedata/interface.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MetadataType } from './hook';
|
||||
export type IMetaDataReturnType = Record<string, Array<Array<string | number>>>;
|
||||
export type IMetaDataReturnJSONType = Record<
|
||||
string,
|
||||
Array<string | number> | string
|
||||
>;
|
||||
|
||||
export interface IMetaDataReturnJSONSettingItem {
|
||||
key: string;
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
}
|
||||
export type IMetaDataReturnJSONSettings = Array<IMetaDataReturnJSONSettingItem>;
|
||||
|
||||
export type IMetaDataTableData = {
|
||||
field: string;
|
||||
description: string;
|
||||
restrictDefinedValues?: boolean;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type IManageModalProps = {
|
||||
title: ReactNode;
|
||||
isShowDescription?: boolean;
|
||||
isDeleteSingleValue?: boolean;
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
tableData?: IMetaDataTableData[];
|
||||
isCanAdd: boolean;
|
||||
type: MetadataType;
|
||||
otherData?: Record<string, any>;
|
||||
isEditField?: boolean;
|
||||
isAddValue?: boolean;
|
||||
isShowValueSwitch?: boolean;
|
||||
isVerticalShowValue?: boolean;
|
||||
success?: (data: any) => void;
|
||||
};
|
||||
|
||||
export interface IManageValuesProps {
|
||||
title: ReactNode;
|
||||
visible: boolean;
|
||||
isEditField?: boolean;
|
||||
isAddValue?: boolean;
|
||||
isShowDescription?: boolean;
|
||||
isShowValueSwitch?: boolean;
|
||||
isVerticalShowValue?: boolean;
|
||||
data: IMetaDataTableData;
|
||||
hideModal: () => void;
|
||||
onSave: (data: IMetaDataTableData) => void;
|
||||
addUpdateValue: (key: string, value: string | string[]) => void;
|
||||
addDeleteValue: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
interface DeleteOperation {
|
||||
key: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface UpdateOperation {
|
||||
key: string;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
export interface MetadataOperations {
|
||||
deletes: DeleteOperation[];
|
||||
updates: UpdateOperation[];
|
||||
}
|
||||
export interface ShowManageMetadataModalOptions {
|
||||
title?: ReactNode | string;
|
||||
}
|
||||
export type ShowManageMetadataModalProps = Partial<IManageModalProps> & {
|
||||
metadata?: IMetaDataTableData[];
|
||||
isCanAdd: boolean;
|
||||
type: MetadataType;
|
||||
record?: Record<string, any>;
|
||||
options?: ShowManageMetadataModalOptions;
|
||||
title?: ReactNode | string;
|
||||
isDeleteSingleValue?: boolean;
|
||||
};
|
||||
373
web/src/pages/dataset/components/metedata/manage-modal.tsx
Normal file
373
web/src/pages/dataset/components/metedata/manage-modal.tsx
Normal file
@ -0,0 +1,373 @@
|
||||
import {
|
||||
ConfirmDeleteDialog,
|
||||
ConfirmDeleteDialogNode,
|
||||
} from '@/components/confirm-delete-dialog';
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { Plus, Settings, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useManageMetaDataModal } from './hook';
|
||||
import { IManageModalProps, IMetaDataTableData } from './interface';
|
||||
import { ManageValuesModal } from './manage-values-modal';
|
||||
export const ManageMetadataModal = (props: IManageModalProps) => {
|
||||
const {
|
||||
title,
|
||||
visible,
|
||||
hideModal,
|
||||
isDeleteSingleValue,
|
||||
tableData: originalTableData,
|
||||
isCanAdd,
|
||||
type: metadataType,
|
||||
otherData,
|
||||
isEditField,
|
||||
isAddValue,
|
||||
isShowDescription = false,
|
||||
isShowValueSwitch = false,
|
||||
isVerticalShowValue = true,
|
||||
success,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const [valueData, setValueData] = useState<IMetaDataTableData>({
|
||||
field: '',
|
||||
description: '',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const [currentValueIndex, setCurrentValueIndex] = useState<number>(0);
|
||||
const [deleteDialogContent, setDeleteDialogContent] = useState({
|
||||
visible: false,
|
||||
title: '',
|
||||
name: '',
|
||||
warnText: '',
|
||||
onOk: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
|
||||
const {
|
||||
tableData,
|
||||
setTableData,
|
||||
handleDeleteSingleValue,
|
||||
handleDeleteSingleRow,
|
||||
handleSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
} = useManageMetaDataModal(originalTableData, metadataType, otherData);
|
||||
|
||||
const {
|
||||
visible: manageValuesVisible,
|
||||
showModal: showManageValuesModal,
|
||||
hideModal: hideManageValuesModal,
|
||||
} = useSetModalState();
|
||||
const hideDeleteModal = () => {
|
||||
setDeleteDialogContent({
|
||||
visible: false,
|
||||
title: '',
|
||||
name: '',
|
||||
warnText: '',
|
||||
onOk: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
};
|
||||
const handAddValueRow = () => {
|
||||
setValueData({
|
||||
field: '',
|
||||
description: '',
|
||||
values: [],
|
||||
});
|
||||
setCurrentValueIndex(tableData.length || 0);
|
||||
showManageValuesModal();
|
||||
};
|
||||
const handleEditValueRow = useCallback(
|
||||
(data: IMetaDataTableData, index: number) => {
|
||||
setCurrentValueIndex(index);
|
||||
setValueData(data);
|
||||
showManageValuesModal();
|
||||
},
|
||||
[showManageValuesModal],
|
||||
);
|
||||
|
||||
const columns: ColumnDef<IMetaDataTableData>[] = useMemo(() => {
|
||||
const cols: ColumnDef<IMetaDataTableData>[] = [
|
||||
{
|
||||
accessorKey: 'field',
|
||||
header: () => <span>{t('knowledgeDetails.metadata.field')}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-accent-primary">
|
||||
{row.getValue('field')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
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: () => <span>{t('knowledgeDetails.metadata.values')}</span>,
|
||||
cell: ({ row }) => {
|
||||
const values = row.getValue('values') as Array<string>;
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{values.length > 0 &&
|
||||
values
|
||||
.filter((value: string, index: number) => index < 2)
|
||||
?.map((value: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
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={() => {
|
||||
handleDeleteSingleValue(
|
||||
row.getValue('field'),
|
||||
value,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{values.length > 2 && (
|
||||
<div className="text-text-secondary self-end">...</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'),
|
||||
name: row.getValue('field'),
|
||||
warnText: t('knowledgeDetails.metadata.deleteWarn'),
|
||||
onOk: () => {
|
||||
hideDeleteModal();
|
||||
handleDeleteSingleRow(row.getValue('field'));
|
||||
},
|
||||
onCancel: () => {
|
||||
hideDeleteModal();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (!isShowDescription) {
|
||||
cols.splice(1, 1);
|
||||
}
|
||||
return cols;
|
||||
}, [
|
||||
handleDeleteSingleRow,
|
||||
t,
|
||||
handleDeleteSingleValue,
|
||||
isShowDescription,
|
||||
isDeleteSingleValue,
|
||||
handleEditValueRow,
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData as IMetaDataTableData[],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleSaveValues = (data: IMetaDataTableData) => {
|
||||
setTableData((prev) => {
|
||||
if (currentValueIndex >= prev.length) {
|
||||
return [...prev, data];
|
||||
} else {
|
||||
return prev.map((item, index) => {
|
||||
if (index === currentValueIndex) {
|
||||
return data;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
maskClosable={false}
|
||||
okText={t('common.save')}
|
||||
onOk={async () => {
|
||||
const res = await handleSave({ callback: hideModal });
|
||||
console.log('data', res);
|
||||
success?.(res);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{t('knowledgeDetails.metadata.metadata')}</div>
|
||||
{isCanAdd && (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
onClick={handAddValueRow}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Table rootClassName="max-h-[800px]">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
<Empty type={EmptyType.Data} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Modal>
|
||||
{manageValuesVisible && (
|
||||
<ManageValuesModal
|
||||
title={<div>{t('knowledgeDetails.metadata.editMetadata')}</div>}
|
||||
visible={manageValuesVisible}
|
||||
hideModal={hideManageValuesModal}
|
||||
data={valueData}
|
||||
onSave={handleSaveValues}
|
||||
addUpdateValue={addUpdateValue}
|
||||
addDeleteValue={addDeleteValue}
|
||||
isEditField={isEditField || isCanAdd}
|
||||
isAddValue={isAddValue || isCanAdd}
|
||||
isShowDescription={isShowDescription}
|
||||
isShowValueSwitch={isShowValueSwitch}
|
||||
isVerticalShowValue={isVerticalShowValue}
|
||||
// handleDeleteSingleValue={handleDeleteSingleValue}
|
||||
// handleDeleteSingleRow={handleDeleteSingleRow}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteDialogContent.visible && (
|
||||
<ConfirmDeleteDialog
|
||||
open={deleteDialogContent.visible}
|
||||
onCancel={deleteDialogContent.onCancel}
|
||||
onOk={deleteDialogContent.onOk}
|
||||
title={deleteDialogContent.title}
|
||||
content={{
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={deleteDialogContent.name}
|
||||
warnText={deleteDialogContent.warnText}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,250 @@
|
||||
import EditTag from '@/components/edit-tag';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IManageValuesProps, IMetaDataTableData } from './interface';
|
||||
|
||||
// Create a separate input component, wrapped with memo to avoid unnecessary re-renders
|
||||
const ValueInputItem = memo(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
onValueChange,
|
||||
onDelete,
|
||||
onBlur,
|
||||
}: {
|
||||
item: string;
|
||||
index: number;
|
||||
onValueChange: (index: number, value: string) => void;
|
||||
onDelete: (index: number) => void;
|
||||
onBlur: (index: number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={`value-item-${index}`}
|
||||
className="flex items-center gap-2.5 w-full"
|
||||
>
|
||||
<div className="flex-1 w-full">
|
||||
<Input
|
||||
value={item}
|
||||
onChange={(e) => onValueChange(index, e.target.value)}
|
||||
onBlur={() => onBlur(index)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="delete"
|
||||
className="border border-border-button px-1 h-6 w-6 rounded-sm"
|
||||
onClick={() => onDelete(index)}
|
||||
>
|
||||
<Trash2 size={14} className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ManageValuesModal = (props: IManageValuesProps) => {
|
||||
const {
|
||||
title,
|
||||
data,
|
||||
isEditField,
|
||||
visible,
|
||||
isAddValue,
|
||||
isShowDescription,
|
||||
isShowValueSwitch,
|
||||
isVerticalShowValue,
|
||||
hideModal,
|
||||
onSave,
|
||||
addUpdateValue,
|
||||
addDeleteValue,
|
||||
} = props;
|
||||
const [metaData, setMetaData] = useState(data);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Use functional update to avoid closure issues
|
||||
const handleChange = useCallback((field: string, value: any) => {
|
||||
setMetaData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Maintain separate state for each input box
|
||||
const [tempValues, setTempValues] = useState<string[]>([...data.values]);
|
||||
|
||||
useEffect(() => {
|
||||
setTempValues([...data.values]);
|
||||
setMetaData(data);
|
||||
}, [data]);
|
||||
|
||||
const handleHideModal = useCallback(() => {
|
||||
hideModal();
|
||||
setMetaData({} as IMetaDataTableData);
|
||||
}, [hideModal]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!metaData.restrictDefinedValues && isShowValueSwitch) {
|
||||
const newMetaData = { ...metaData, values: [] };
|
||||
onSave(newMetaData);
|
||||
} else {
|
||||
onSave(metaData);
|
||||
}
|
||||
handleHideModal();
|
||||
}, [metaData, onSave, handleHideModal, isShowValueSwitch]);
|
||||
|
||||
// Handle value changes, only update temporary state
|
||||
const handleValueChange = useCallback((index: number, value: string) => {
|
||||
setTempValues((prev) => {
|
||||
const newValues = [...prev];
|
||||
newValues[index] = value;
|
||||
|
||||
return newValues;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle blur event, synchronize to main state
|
||||
const handleValueBlur = useCallback(() => {
|
||||
addUpdateValue(metaData.field, [...tempValues]);
|
||||
handleChange('values', [...tempValues]);
|
||||
}, [handleChange, tempValues, metaData, addUpdateValue]);
|
||||
|
||||
// Handle delete operation
|
||||
const handleDelete = useCallback(
|
||||
(index: number) => {
|
||||
setTempValues((prev) => {
|
||||
const newTempValues = [...prev];
|
||||
addDeleteValue(metaData.field, newTempValues[index]);
|
||||
newTempValues.splice(index, 1);
|
||||
return newTempValues;
|
||||
});
|
||||
|
||||
// Synchronize to main state
|
||||
setMetaData((prev) => {
|
||||
const newMetaDataValues = [...prev.values];
|
||||
newMetaDataValues.splice(index, 1);
|
||||
return {
|
||||
...prev,
|
||||
values: newMetaDataValues,
|
||||
};
|
||||
});
|
||||
},
|
||||
[addDeleteValue, metaData],
|
||||
);
|
||||
|
||||
// Handle adding new value
|
||||
const handleAddValue = useCallback(() => {
|
||||
setTempValues((prev) => [...prev, '']);
|
||||
|
||||
// Synchronize to main state
|
||||
setMetaData((prev) => ({
|
||||
...prev,
|
||||
values: [...prev.values, ''],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleHideModal}
|
||||
className="!w-[460px]"
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{!isEditField && (
|
||||
<div className="text-base p-5 border border-border-button rounded-lg">
|
||||
{metaData.field}
|
||||
</div>
|
||||
)}
|
||||
{isEditField && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.fieldName')}</div>
|
||||
<div>
|
||||
<Input
|
||||
value={metaData.field}
|
||||
onChange={(e) => {
|
||||
handleChange('field', e.target?.value || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isShowDescription && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.description')}</div>
|
||||
<div>
|
||||
<Textarea
|
||||
value={metaData.description}
|
||||
onChange={(e) => {
|
||||
handleChange('description', e.target?.value || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isShowValueSwitch && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>{t('knowledgeDetails.metadata.restrictDefinedValues')}</div>
|
||||
<div>
|
||||
<Switch
|
||||
checked={metaData.restrictDefinedValues || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange('restrictDefinedValues', checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{metaData.restrictDefinedValues && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{t('knowledgeDetails.metadata.values')}</div>
|
||||
{isAddValue && isVerticalShowValue && (
|
||||
<div>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="border border-border-button"
|
||||
onClick={handleAddValue}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isVerticalShowValue && (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{tempValues?.map((item, index) => {
|
||||
return (
|
||||
<ValueInputItem
|
||||
key={`value-item-${index}`}
|
||||
item={item}
|
||||
index={index}
|
||||
onValueChange={handleValueChange}
|
||||
onDelete={handleDelete}
|
||||
onBlur={handleValueBlur}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!isVerticalShowValue && (
|
||||
<EditTag
|
||||
value={metaData.values}
|
||||
onChange={(value) => handleChange('values', value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user