@lobehub/chat 1.76.0 → 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/Dockerfile +3 -2
- package/Dockerfile.database +3 -1
- package/Dockerfile.pglite +3 -1
- package/changelog/v1.json +18 -0
- package/locales/ar/common.json +12 -1
- package/locales/ar/error.json +10 -0
- package/locales/ar/models.json +12 -6
- package/locales/ar/setting.json +28 -0
- package/locales/bg-BG/common.json +12 -1
- package/locales/bg-BG/error.json +10 -0
- package/locales/bg-BG/models.json +12 -6
- package/locales/bg-BG/setting.json +28 -0
- package/locales/de-DE/common.json +12 -1
- package/locales/de-DE/error.json +10 -0
- package/locales/de-DE/models.json +12 -6
- package/locales/de-DE/setting.json +28 -0
- package/locales/en-US/common.json +12 -1
- package/locales/en-US/error.json +10 -0
- package/locales/en-US/models.json +12 -6
- package/locales/en-US/setting.json +28 -0
- package/locales/es-ES/common.json +12 -1
- package/locales/es-ES/error.json +10 -0
- package/locales/es-ES/models.json +12 -6
- package/locales/es-ES/setting.json +28 -0
- package/locales/fa-IR/common.json +12 -1
- package/locales/fa-IR/error.json +10 -0
- package/locales/fa-IR/models.json +12 -6
- package/locales/fa-IR/setting.json +28 -0
- package/locales/fr-FR/common.json +12 -1
- package/locales/fr-FR/error.json +10 -0
- package/locales/fr-FR/models.json +12 -6
- package/locales/fr-FR/setting.json +28 -0
- package/locales/it-IT/common.json +12 -1
- package/locales/it-IT/error.json +10 -0
- package/locales/it-IT/models.json +12 -6
- package/locales/it-IT/setting.json +28 -0
- package/locales/ja-JP/common.json +12 -1
- package/locales/ja-JP/error.json +10 -0
- package/locales/ja-JP/models.json +12 -6
- package/locales/ja-JP/setting.json +28 -0
- package/locales/ko-KR/common.json +12 -1
- package/locales/ko-KR/error.json +10 -0
- package/locales/ko-KR/models.json +12 -6
- package/locales/ko-KR/setting.json +28 -0
- package/locales/nl-NL/common.json +12 -1
- package/locales/nl-NL/error.json +10 -0
- package/locales/nl-NL/models.json +12 -6
- package/locales/nl-NL/setting.json +28 -0
- package/locales/pl-PL/common.json +12 -1
- package/locales/pl-PL/error.json +10 -0
- package/locales/pl-PL/models.json +12 -6
- package/locales/pl-PL/setting.json +28 -0
- package/locales/pt-BR/common.json +12 -1
- package/locales/pt-BR/error.json +10 -0
- package/locales/pt-BR/models.json +12 -6
- package/locales/pt-BR/setting.json +28 -0
- package/locales/ru-RU/common.json +12 -1
- package/locales/ru-RU/error.json +10 -0
- package/locales/ru-RU/models.json +12 -6
- package/locales/ru-RU/setting.json +28 -0
- package/locales/tr-TR/common.json +12 -1
- package/locales/tr-TR/error.json +10 -0
- package/locales/tr-TR/models.json +12 -6
- package/locales/tr-TR/setting.json +28 -0
- package/locales/vi-VN/common.json +12 -1
- package/locales/vi-VN/error.json +10 -0
- package/locales/vi-VN/models.json +12 -6
- package/locales/vi-VN/setting.json +28 -0
- package/locales/zh-CN/common.json +12 -1
- package/locales/zh-CN/error.json +10 -0
- package/locales/zh-CN/models.json +12 -6
- package/locales/zh-CN/setting.json +28 -0
- package/locales/zh-TW/common.json +12 -1
- package/locales/zh-TW/error.json +10 -0
- package/locales/zh-TW/models.json +12 -6
- package/locales/zh-TW/setting.json +28 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +1 -1
- package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
- package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
- package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +133 -0
- package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
- package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
- package/src/app/[variants]/(main)/settings/tts/features/const.tsx +4 -0
- package/src/components/GroupIcon/index.tsx +25 -0
- package/src/components/IndexCard/index.tsx +143 -0
- package/src/components/ProgressItem/index.tsx +75 -0
- package/src/config/aiModels/openai.ts +10 -0
- package/src/database/repositories/dataExporter/index.test.ts +330 -0
- package/src/database/repositories/dataExporter/index.ts +216 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
- package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
- package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
- package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
- package/src/database/repositories/dataImporter/index.ts +684 -289
- package/src/features/DataImporter/ImportDetail.tsx +203 -0
- package/src/features/DataImporter/SuccessResult.tsx +22 -6
- package/src/features/DataImporter/_deprecated.ts +43 -0
- package/src/features/DataImporter/config.ts +21 -0
- package/src/features/DataImporter/index.tsx +112 -31
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
- package/src/features/User/UserPanel/useMenu.tsx +0 -35
- package/src/features/User/__tests__/useMenu.test.tsx +0 -2
- package/src/locales/default/common.ts +11 -0
- package/src/locales/default/error.ts +10 -0
- package/src/locales/default/setting.ts +28 -0
- package/src/server/routers/lambda/exporter.ts +25 -0
- package/src/server/routers/lambda/importer.ts +19 -3
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/services/config.ts +80 -135
- package/src/services/export/_deprecated.ts +155 -0
- package/src/services/export/client.ts +15 -0
- package/src/services/export/index.ts +6 -0
- package/src/services/export/server.ts +9 -0
- package/src/services/export/type.ts +5 -0
- package/src/services/import/_deprecated.ts +42 -1
- package/src/services/import/client.test.ts +1 -1
- package/src/services/import/client.ts +30 -1
- package/src/services/import/server.ts +70 -2
- package/src/services/import/type.ts +10 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/types/export.ts +11 -0
- package/src/types/exportConfig.ts +2 -0
- package/src/types/importer.ts +15 -0
- package/src/types/user/settings/tts.ts +1 -1
- package/src/utils/client/exportFile.ts +21 -0
- package/vitest.config.ts +1 -1
- package/src/utils/config.ts +0 -109
- /package/src/database/repositories/dataImporter/{__tests__ → deprecated/__tests__}/fixtures/messages.json +0 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Modal } from '@lobehub/ui';
|
4
|
+
import { Button, Table, Typography } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import { Info } from 'lucide-react';
|
7
|
+
import { useState } from 'react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { ImportPgDataStructure } from '@/types/export';
|
12
|
+
|
13
|
+
const { Text } = Typography;
|
14
|
+
|
15
|
+
const getNonEmptyTables = (data: ImportPgDataStructure) => {
|
16
|
+
const result = [];
|
17
|
+
|
18
|
+
for (const [key, value] of Object.entries(data.data)) {
|
19
|
+
if (Array.isArray(value) && value.length > 0) {
|
20
|
+
result.push({
|
21
|
+
count: value.length,
|
22
|
+
name: key,
|
23
|
+
});
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
return result;
|
28
|
+
};
|
29
|
+
|
30
|
+
const getTotalRecords = (tables: { count: number; name: string }[]): number => {
|
31
|
+
return tables.reduce((sum, table) => sum + table.count, 0);
|
32
|
+
};
|
33
|
+
|
34
|
+
const useStyles = createStyles(({ token, css }) => {
|
35
|
+
return {
|
36
|
+
duplicateAlert: css`
|
37
|
+
margin-block-start: ${token.marginMD}px;
|
38
|
+
padding: ${token.paddingMD}px;
|
39
|
+
border: 1px solid ${token.colorWarningBorder};
|
40
|
+
border-radius: ${token.borderRadiusLG}px;
|
41
|
+
|
42
|
+
background-color: ${token.colorWarningBg};
|
43
|
+
`,
|
44
|
+
duplicateDescription: css`
|
45
|
+
margin-block-start: ${token.marginXS}px;
|
46
|
+
font-size: ${token.fontSizeSM}px;
|
47
|
+
color: ${token.colorTextSecondary};
|
48
|
+
`,
|
49
|
+
duplicateOptions: css`
|
50
|
+
margin-block-start: ${token.marginSM}px;
|
51
|
+
`,
|
52
|
+
duplicateTag: css`
|
53
|
+
border-color: ${token.colorWarningBorder};
|
54
|
+
color: ${token.colorWarning};
|
55
|
+
background-color: ${token.colorWarningBg};
|
56
|
+
`,
|
57
|
+
hash: css`
|
58
|
+
font-family: ${token.fontFamilyCode};
|
59
|
+
font-size: 12px;
|
60
|
+
color: ${token.colorTextTertiary};
|
61
|
+
`,
|
62
|
+
infoIcon: css`
|
63
|
+
color: ${token.colorTextSecondary};
|
64
|
+
`,
|
65
|
+
modalContent: css`
|
66
|
+
padding-block: ${token.paddingMD}px;
|
67
|
+
padding-inline: 0;
|
68
|
+
`,
|
69
|
+
successIcon: css`
|
70
|
+
color: ${token.colorSuccess};
|
71
|
+
`,
|
72
|
+
tableContainer: css`
|
73
|
+
overflow: hidden;
|
74
|
+
border: 1px solid ${token.colorBorderSecondary};
|
75
|
+
border-radius: ${token.borderRadiusLG}px;
|
76
|
+
`,
|
77
|
+
tableName: css`
|
78
|
+
font-family: ${token.fontFamilyCode};
|
79
|
+
`,
|
80
|
+
warningIcon: css`
|
81
|
+
color: ${token.colorWarning};
|
82
|
+
`,
|
83
|
+
};
|
84
|
+
});
|
85
|
+
|
86
|
+
interface ImportPreviewModalProps {
|
87
|
+
importData: ImportPgDataStructure;
|
88
|
+
onCancel?: () => void;
|
89
|
+
onConfirm?: (overwriteExisting: boolean) => void;
|
90
|
+
onOpenChange: (open: boolean) => void;
|
91
|
+
open: boolean;
|
92
|
+
}
|
93
|
+
|
94
|
+
const ImportPreviewModal = ({
|
95
|
+
open = true,
|
96
|
+
onOpenChange = () => {},
|
97
|
+
onConfirm = () => {},
|
98
|
+
onCancel = () => {},
|
99
|
+
importData,
|
100
|
+
}: ImportPreviewModalProps) => {
|
101
|
+
const { t } = useTranslation('common');
|
102
|
+
const { styles } = useStyles();
|
103
|
+
const [duplicateAction] = useState<string>('skip');
|
104
|
+
const tables = getNonEmptyTables(importData);
|
105
|
+
const totalRecords = getTotalRecords(tables);
|
106
|
+
|
107
|
+
// 表格列定义
|
108
|
+
const columns = [
|
109
|
+
{
|
110
|
+
dataIndex: 'name',
|
111
|
+
key: 'name',
|
112
|
+
render: (text: string) => <div className={styles.tableName}>{text}</div>,
|
113
|
+
title: t('importPreview.tables.name'),
|
114
|
+
},
|
115
|
+
{
|
116
|
+
dataIndex: 'count',
|
117
|
+
key: 'count',
|
118
|
+
title: t('importPreview.tables.count'),
|
119
|
+
},
|
120
|
+
];
|
121
|
+
|
122
|
+
const handleConfirm = () => {
|
123
|
+
onConfirm(duplicateAction === 'overwrite');
|
124
|
+
onOpenChange(false);
|
125
|
+
};
|
126
|
+
|
127
|
+
return (
|
128
|
+
<Modal
|
129
|
+
footer={[
|
130
|
+
<Button
|
131
|
+
key="cancel"
|
132
|
+
onClick={() => {
|
133
|
+
onOpenChange(false);
|
134
|
+
onCancel();
|
135
|
+
}}
|
136
|
+
>
|
137
|
+
{t('cancel')}
|
138
|
+
</Button>,
|
139
|
+
<Button key="confirm" onClick={handleConfirm} type="primary">
|
140
|
+
{t('importPreview.confirmImport')}
|
141
|
+
</Button>,
|
142
|
+
]}
|
143
|
+
onCancel={() => onOpenChange(false)}
|
144
|
+
open={open}
|
145
|
+
title={t('importPreview.title')}
|
146
|
+
width={700}
|
147
|
+
>
|
148
|
+
<div className={styles.modalContent}>
|
149
|
+
<Flexbox gap={16}>
|
150
|
+
<Flexbox gap={4}>
|
151
|
+
<Flexbox align="center" horizontal justify="space-between" width="100%">
|
152
|
+
<Flexbox align="center" gap={8} horizontal>
|
153
|
+
<Info className={styles.infoIcon} size={16} />
|
154
|
+
<Text strong>{t('importPreview.totalRecords', { count: totalRecords })}</Text>
|
155
|
+
</Flexbox>
|
156
|
+
<Flexbox horizontal>
|
157
|
+
<Text type="secondary">
|
158
|
+
{t('importPreview.totalTables', { count: tables.length })}
|
159
|
+
</Text>
|
160
|
+
</Flexbox>
|
161
|
+
</Flexbox>
|
162
|
+
<Flexbox className={styles.hash} gap={4} horizontal>
|
163
|
+
Hash: <span>{importData.schemaHash}</span>
|
164
|
+
</Flexbox>
|
165
|
+
</Flexbox>
|
166
|
+
|
167
|
+
<div className={styles.tableContainer}>
|
168
|
+
<Table
|
169
|
+
columns={columns}
|
170
|
+
dataSource={tables}
|
171
|
+
pagination={false}
|
172
|
+
rowKey="name"
|
173
|
+
scroll={{ y: 350 }}
|
174
|
+
size="small"
|
175
|
+
/>
|
176
|
+
</div>
|
177
|
+
|
178
|
+
{/*<Flexbox>*/}
|
179
|
+
{/* 重复数据处理方式:*/}
|
180
|
+
{/* <div className={styles.duplicateOptions}>*/}
|
181
|
+
{/* <Radio.Group*/}
|
182
|
+
{/* onChange={(e) => setDuplicateAction(e.target.value)}*/}
|
183
|
+
{/* value={duplicateAction}*/}
|
184
|
+
{/* >*/}
|
185
|
+
{/* <Space>*/}
|
186
|
+
{/* <Radio value="skip">跳过</Radio>*/}
|
187
|
+
{/* <Radio value="overwrite">覆盖</Radio>*/}
|
188
|
+
{/* </Space>*/}
|
189
|
+
{/* </Radio.Group>*/}
|
190
|
+
{/* </div>*/}
|
191
|
+
{/* <div className={styles.duplicateDescription}>*/}
|
192
|
+
{/* {duplicateAction === 'skip'*/}
|
193
|
+
{/* ? '选择跳过将仅导入不重复的数据,保留现有数据不变。'*/}
|
194
|
+
{/* : '选择覆盖将使用导入数据替换系统中具有相同 ID 的现有记录。'}*/}
|
195
|
+
{/* </div>*/}
|
196
|
+
{/*</Flexbox>*/}
|
197
|
+
</Flexbox>
|
198
|
+
</div>
|
199
|
+
</Modal>
|
200
|
+
);
|
201
|
+
};
|
202
|
+
|
203
|
+
export default ImportPreviewModal;
|
@@ -2,17 +2,27 @@
|
|
2
2
|
|
3
3
|
import { Icon } from '@lobehub/ui';
|
4
4
|
import { Button, Result, Table } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
5
6
|
import { CheckCircle } from 'lucide-react';
|
6
7
|
import React, { memo } from 'react';
|
7
8
|
import { useTranslation } from 'react-i18next';
|
8
9
|
import { Flexbox } from 'react-layout-kit';
|
9
10
|
|
11
|
+
const useStyles = createStyles(({ token, css }) => {
|
12
|
+
return {
|
13
|
+
zeroCell: css`
|
14
|
+
color: ${token.colorTextQuaternary};
|
15
|
+
`,
|
16
|
+
};
|
17
|
+
});
|
18
|
+
|
10
19
|
interface SuccessResultProps {
|
11
20
|
dataSource?: {
|
12
21
|
added: number;
|
13
22
|
error: number;
|
14
23
|
skips: number;
|
15
24
|
title: string;
|
25
|
+
updated: number;
|
16
26
|
}[];
|
17
27
|
duration: number;
|
18
28
|
onClickFinish?: () => void;
|
@@ -20,7 +30,11 @@ interface SuccessResultProps {
|
|
20
30
|
|
21
31
|
const SuccessResult = memo<SuccessResultProps>(({ duration, dataSource, onClickFinish }) => {
|
22
32
|
const { t } = useTranslation('common');
|
33
|
+
const { styles } = useStyles();
|
23
34
|
|
35
|
+
const cellRender = (text: string) => {
|
36
|
+
return text ? text : <span className={styles.zeroCell}>0</span>;
|
37
|
+
};
|
24
38
|
return (
|
25
39
|
<Result
|
26
40
|
extra={
|
@@ -30,24 +44,26 @@ const SuccessResult = memo<SuccessResultProps>(({ duration, dataSource, onClickF
|
|
30
44
|
}
|
31
45
|
icon={<Icon icon={CheckCircle} />}
|
32
46
|
status={'success'}
|
33
|
-
style={{ paddingBlock: 24 }}
|
47
|
+
style={{ paddingBlock: 24, paddingInline: 0 }}
|
34
48
|
subTitle={
|
35
49
|
// if there is no importData, means it's only import the settings
|
36
50
|
!dataSource ? (
|
37
51
|
t('importModal.finish.onlySettings')
|
38
52
|
) : (
|
39
|
-
<Flexbox gap={16} width={
|
53
|
+
<Flexbox gap={16} width={500}>
|
40
54
|
{t('importModal.finish.subTitle', { duration: (duration / 1000).toFixed(2) })}
|
41
55
|
<Table
|
42
56
|
bordered
|
43
57
|
columns={[
|
44
|
-
{ dataIndex: 'title', title: t('importModal.result.type') },
|
45
|
-
{ dataIndex: 'added', title: t('importModal.result.added') },
|
46
|
-
{ dataIndex: 'skips', title: t('importModal.result.skips') },
|
47
|
-
{ dataIndex: 'error', title: t('importModal.result.errors') },
|
58
|
+
{ dataIndex: 'title', render: cellRender, title: t('importModal.result.type') },
|
59
|
+
{ dataIndex: 'added', render: cellRender, title: t('importModal.result.added') },
|
60
|
+
{ dataIndex: 'skips', render: cellRender, title: t('importModal.result.skips') },
|
61
|
+
{ dataIndex: 'error', render: cellRender, title: t('importModal.result.errors') },
|
62
|
+
{ dataIndex: 'updated', render: cellRender, title: t('importModal.result.update') },
|
48
63
|
]}
|
49
64
|
dataSource={dataSource}
|
50
65
|
pagination={false}
|
66
|
+
rowKey={'title'}
|
51
67
|
size={'small'}
|
52
68
|
/>
|
53
69
|
</Flexbox>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { t } from 'i18next';
|
2
|
+
|
3
|
+
import { notification } from '@/components/AntdStaticMethods';
|
4
|
+
import { Migration } from '@/migrations';
|
5
|
+
import { ConfigFile } from '@/types/exportConfig';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* V2 删除该方法
|
9
|
+
* 不再需要 Migration.migrate
|
10
|
+
* @deprecated
|
11
|
+
*/
|
12
|
+
export const importConfigFile = async (
|
13
|
+
file: File,
|
14
|
+
onConfigImport: (config: ConfigFile) => Promise<void>,
|
15
|
+
) => {
|
16
|
+
const text = await file.text();
|
17
|
+
|
18
|
+
try {
|
19
|
+
const config = JSON.parse(text);
|
20
|
+
|
21
|
+
// it means the config file is exported from a newer version
|
22
|
+
if ('schemaHash' in config) {
|
23
|
+
notification.error({
|
24
|
+
description: t('import.incompatible.description', { ns: 'error' }),
|
25
|
+
message: t('import.incompatible.title', { ns: 'error' }),
|
26
|
+
});
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
const { state, version } = Migration.migrate(config);
|
31
|
+
|
32
|
+
await onConfigImport({ ...config, state, version });
|
33
|
+
} catch (error) {
|
34
|
+
console.error(error);
|
35
|
+
notification.error({
|
36
|
+
description: t('import.importConfigFile.description', {
|
37
|
+
ns: 'error',
|
38
|
+
reason: (error as any).message,
|
39
|
+
}),
|
40
|
+
message: t('import.importConfigFile.title', { ns: 'error' }),
|
41
|
+
});
|
42
|
+
}
|
43
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { t } from 'i18next';
|
2
|
+
|
3
|
+
import { notification } from '@/components/AntdStaticMethods';
|
4
|
+
import { ImportPgDataStructure } from '@/types/export';
|
5
|
+
|
6
|
+
export const parseConfigFile = async (file: File): Promise<ImportPgDataStructure | undefined> => {
|
7
|
+
const text = await file.text();
|
8
|
+
|
9
|
+
try {
|
10
|
+
return JSON.parse(text);
|
11
|
+
} catch (error) {
|
12
|
+
console.error(error);
|
13
|
+
notification.error({
|
14
|
+
description: t('import.importConfigFile.description', {
|
15
|
+
ns: 'error',
|
16
|
+
reason: (error as any).message,
|
17
|
+
}),
|
18
|
+
message: t('import.importConfigFile.title', { ns: 'error' }),
|
19
|
+
});
|
20
|
+
}
|
21
|
+
};
|
@@ -8,16 +8,21 @@ import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Center } from 'react-layout-kit';
|
9
9
|
|
10
10
|
import DataStyleModal from '@/components/DataStyleModal';
|
11
|
-
import {
|
11
|
+
import { importService } from '@/services/import';
|
12
|
+
import { ImportResult, ImportResults } from '@/services/import/_deprecated';
|
12
13
|
import { useChatStore } from '@/store/chat';
|
13
14
|
import { useSessionStore } from '@/store/session';
|
14
|
-
import {
|
15
|
-
import {
|
15
|
+
import { ImportPgDataStructure } from '@/types/export';
|
16
|
+
import { ConfigFile } from '@/types/exportConfig';
|
17
|
+
import { ErrorShape, FileUploadState, ImportStage, OnImportCallbacks } from '@/types/importer';
|
16
18
|
|
17
19
|
import ImportError from './Error';
|
18
20
|
import { FileUploading } from './FileUploading';
|
21
|
+
import ImportPreviewModal from './ImportDetail';
|
19
22
|
import DataLoading from './Loading';
|
20
23
|
import SuccessResult from './SuccessResult';
|
24
|
+
import { importConfigFile } from './_deprecated';
|
25
|
+
import { parseConfigFile } from './config';
|
21
26
|
|
22
27
|
const useStyles = createStyles(({ css }) => ({
|
23
28
|
children: css`
|
@@ -50,13 +55,16 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
50
55
|
|
51
56
|
const [fileUploadingState, setUploadingState] = useState<FileUploadState | undefined>();
|
52
57
|
const [importError, setImportError] = useState<ErrorShape | undefined>();
|
53
|
-
const [
|
58
|
+
const [importResults, setImportResults] = useState<ImportResults | undefined>();
|
59
|
+
const [showImportModal, setShowImportModal] = useState(false);
|
60
|
+
const [importPgData, setImportPgData] = useState<ImportPgDataStructure | undefined>(undefined);
|
54
61
|
|
55
62
|
const dataSource = useMemo(() => {
|
56
|
-
if (!
|
63
|
+
if (!importResults) return;
|
57
64
|
|
58
|
-
const { type, ...res } =
|
65
|
+
const { type, ...res } = importResults;
|
59
66
|
|
67
|
+
console.log(res);
|
60
68
|
if (type === 'settings') return;
|
61
69
|
|
62
70
|
return Object.entries(res)
|
@@ -65,15 +73,16 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
65
73
|
added: value.added,
|
66
74
|
error: value.errors,
|
67
75
|
skips: value.skips,
|
68
|
-
title:
|
76
|
+
title: item,
|
77
|
+
updated: value.updated || 0,
|
69
78
|
}));
|
70
|
-
}, [
|
79
|
+
}, [importResults]);
|
71
80
|
|
72
81
|
const isFinished = importState === ImportStage.Success || importState === ImportStage.Error;
|
73
82
|
|
74
83
|
const closeModal = () => {
|
75
84
|
setImportState(ImportStage.Finished);
|
76
|
-
|
85
|
+
setImportResults(undefined);
|
77
86
|
setImportError(undefined);
|
78
87
|
setUploadingState(undefined);
|
79
88
|
|
@@ -114,7 +123,7 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
114
123
|
|
115
124
|
case ImportStage.Success: {
|
116
125
|
return (
|
117
|
-
<Center gap={24} paddingInline={
|
126
|
+
<Center gap={24} paddingInline={16}>
|
118
127
|
<SuccessResult dataSource={dataSource} duration={duration} onClickFinish={closeModal} />
|
119
128
|
</Center>
|
120
129
|
);
|
@@ -139,35 +148,74 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
139
148
|
icon={ImportIcon}
|
140
149
|
open={importState !== ImportStage.Start && importState !== ImportStage.Finished}
|
141
150
|
title={t('importModal.title')}
|
142
|
-
width={isFinished ?
|
151
|
+
width={isFinished ? 600 : 400}
|
143
152
|
>
|
144
153
|
{content}
|
145
154
|
</DataStyleModal>
|
146
155
|
<Upload
|
156
|
+
accept={'application/json'}
|
147
157
|
beforeUpload={async (file) => {
|
148
|
-
await
|
149
|
-
|
158
|
+
const config = await parseConfigFile(file);
|
159
|
+
if (!config) return false;
|
150
160
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
161
|
+
if (!('schemaHash' in config)) {
|
162
|
+
// TODO: remove in V2
|
163
|
+
await importConfigFile(file, async (config) => {
|
164
|
+
setImportState(ImportStage.Preparing);
|
165
|
+
console.log(config);
|
166
|
+
|
167
|
+
const importConfigState = async (
|
168
|
+
config: ConfigFile,
|
169
|
+
callbacks?: OnImportCallbacks,
|
170
|
+
): Promise<void> => {
|
171
|
+
if (config.exportType === 'settings') {
|
172
|
+
await importService.importSettings(config.state.settings);
|
173
|
+
callbacks?.onStageChange?.(ImportStage.Success);
|
174
|
+
return;
|
175
|
+
}
|
176
|
+
|
177
|
+
if (config.exportType === 'all') {
|
178
|
+
await importService.importSettings(config.state.settings);
|
179
|
+
}
|
180
|
+
|
181
|
+
await importService.importData(
|
182
|
+
{
|
183
|
+
messages: (config.state as any).messages || [],
|
184
|
+
sessionGroups: (config.state as any).sessionGroups || [],
|
185
|
+
sessions: (config.state as any).sessions || [],
|
186
|
+
topics: (config.state as any).topics || [],
|
187
|
+
version: config.version,
|
188
|
+
},
|
189
|
+
callbacks,
|
190
|
+
);
|
191
|
+
};
|
192
|
+
|
193
|
+
await importConfigState(config, {
|
194
|
+
onError: (error) => {
|
195
|
+
setImportError(error);
|
196
|
+
},
|
197
|
+
onFileUploading: (state) => {
|
198
|
+
setUploadingState(state);
|
199
|
+
},
|
200
|
+
onStageChange: (stage) => {
|
201
|
+
setImportState(stage);
|
202
|
+
},
|
203
|
+
onSuccess: (data, duration) => {
|
204
|
+
if (data) setImportResults(data);
|
205
|
+
setDuration(duration);
|
206
|
+
},
|
207
|
+
});
|
208
|
+
|
209
|
+
await refreshSessions();
|
210
|
+
await refreshMessages();
|
211
|
+
await refreshTopics();
|
165
212
|
});
|
166
213
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
214
|
+
return false;
|
215
|
+
}
|
216
|
+
|
217
|
+
setImportPgData(config);
|
218
|
+
setShowImportModal(true);
|
171
219
|
|
172
220
|
return false;
|
173
221
|
}}
|
@@ -178,6 +226,39 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
178
226
|
{/* a very hackable solution: add a pseudo before to have a large hot zone */}
|
179
227
|
<div className={styles.children}>{children}</div>
|
180
228
|
</Upload>
|
229
|
+
{importPgData && (
|
230
|
+
<ImportPreviewModal
|
231
|
+
importData={importPgData}
|
232
|
+
onConfirm={async (overwriteExisting) => {
|
233
|
+
setImportState(ImportStage.Preparing);
|
234
|
+
|
235
|
+
await importService.importPgData(importPgData, {
|
236
|
+
callbacks: {
|
237
|
+
onError: (error) => {
|
238
|
+
setImportError(error);
|
239
|
+
},
|
240
|
+
onFileUploading: (state) => {
|
241
|
+
setUploadingState(state);
|
242
|
+
},
|
243
|
+
onStageChange: (stage) => {
|
244
|
+
setImportState(stage);
|
245
|
+
},
|
246
|
+
onSuccess: (data, duration) => {
|
247
|
+
if (data) setImportResults(data);
|
248
|
+
setDuration(duration);
|
249
|
+
},
|
250
|
+
},
|
251
|
+
overwriteExisting,
|
252
|
+
});
|
253
|
+
|
254
|
+
await refreshSessions();
|
255
|
+
await refreshMessages();
|
256
|
+
await refreshTopics();
|
257
|
+
}}
|
258
|
+
onOpenChange={setShowImportModal}
|
259
|
+
open={showImportModal}
|
260
|
+
/>
|
261
|
+
)}
|
181
262
|
</>
|
182
263
|
);
|
183
264
|
});
|
@@ -5,6 +5,8 @@ import React from 'react';
|
|
5
5
|
import { Center, Flexbox } from 'react-layout-kit';
|
6
6
|
import { mutate } from 'swr';
|
7
7
|
|
8
|
+
import { exportService } from '@/services/export';
|
9
|
+
|
8
10
|
import Header from '../../features/Header';
|
9
11
|
import Table from '../../features/Table';
|
10
12
|
import { FETCH_TABLE_DATA_KEY, usePgTable, useTableColumns } from '../usePgTable';
|
@@ -39,6 +41,10 @@ const DataTable = ({ tableName }: DataTableProps) => {
|
|
39
41
|
},
|
40
42
|
{
|
41
43
|
icon: Download,
|
44
|
+
onClick: async () => {
|
45
|
+
const data = await exportService.exportData();
|
46
|
+
console.log(data);
|
47
|
+
},
|
42
48
|
title: 'Export',
|
43
49
|
},
|
44
50
|
{
|
@@ -9,7 +9,6 @@ import {
|
|
9
9
|
Feather,
|
10
10
|
FileClockIcon,
|
11
11
|
HardDriveDownload,
|
12
|
-
HardDriveUpload,
|
13
12
|
LifeBuoy,
|
14
13
|
LogOut,
|
15
14
|
Mail,
|
@@ -32,10 +31,8 @@ import {
|
|
32
31
|
UTM_SOURCE,
|
33
32
|
mailTo,
|
34
33
|
} from '@/const/url';
|
35
|
-
import { isServerMode } from '@/const/version';
|
36
34
|
import DataImporter from '@/features/DataImporter';
|
37
35
|
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
38
|
-
import { configService } from '@/services/config';
|
39
36
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
40
37
|
import { useUserStore } from '@/store/user';
|
41
38
|
import { authSelectors } from '@/store/user/selectors';
|
@@ -121,38 +118,6 @@ export const useMenu = () => {
|
|
121
118
|
key: 'import',
|
122
119
|
label: <DataImporter>{t('import')}</DataImporter>,
|
123
120
|
},
|
124
|
-
isServerMode
|
125
|
-
? null
|
126
|
-
: {
|
127
|
-
children: [
|
128
|
-
{
|
129
|
-
key: 'allAgent',
|
130
|
-
label: t('exportType.allAgent'),
|
131
|
-
onClick: configService.exportAgents,
|
132
|
-
},
|
133
|
-
{
|
134
|
-
key: 'allAgentWithMessage',
|
135
|
-
label: t('exportType.allAgentWithMessage'),
|
136
|
-
onClick: configService.exportSessions,
|
137
|
-
},
|
138
|
-
{
|
139
|
-
key: 'globalSetting',
|
140
|
-
label: t('exportType.globalSetting'),
|
141
|
-
onClick: configService.exportSettings,
|
142
|
-
},
|
143
|
-
{
|
144
|
-
type: 'divider',
|
145
|
-
},
|
146
|
-
{
|
147
|
-
key: 'all',
|
148
|
-
label: t('exportType.all'),
|
149
|
-
onClick: configService.exportAll,
|
150
|
-
},
|
151
|
-
],
|
152
|
-
icon: <Icon icon={HardDriveUpload} />,
|
153
|
-
key: 'export',
|
154
|
-
label: t('export'),
|
155
|
-
},
|
156
121
|
{
|
157
122
|
type: 'divider',
|
158
123
|
},
|
@@ -81,7 +81,6 @@ describe('useMenu', () => {
|
|
81
81
|
expect(mainItems?.some((item) => item?.key === 'profile')).toBe(true);
|
82
82
|
expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
|
83
83
|
expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
|
84
|
-
expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
|
85
84
|
expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
|
86
85
|
expect(logoutItems.some((item) => item?.key === 'logout')).toBe(true);
|
87
86
|
});
|
@@ -100,7 +99,6 @@ describe('useMenu', () => {
|
|
100
99
|
expect(mainItems?.some((item) => item?.key === 'profile')).toBe(true);
|
101
100
|
expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
|
102
101
|
expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
|
103
|
-
expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
|
104
102
|
expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
|
105
103
|
expect(logoutItems.some((item) => item?.key === 'logout')).toBe(false);
|
106
104
|
});
|
@@ -209,6 +209,7 @@ export default {
|
|
209
209
|
skips: '重复跳过',
|
210
210
|
topics: '话题',
|
211
211
|
type: '数据类型',
|
212
|
+
update: '记录更新',
|
212
213
|
},
|
213
214
|
title: '导入数据',
|
214
215
|
uploading: {
|
@@ -217,6 +218,16 @@ export default {
|
|
217
218
|
speed: '上传速度',
|
218
219
|
},
|
219
220
|
},
|
221
|
+
importPreview: {
|
222
|
+
confirmImport: '确认导入',
|
223
|
+
tables: {
|
224
|
+
count: '记录数',
|
225
|
+
name: '表名',
|
226
|
+
},
|
227
|
+
title: '导入数据预览',
|
228
|
+
totalRecords:"总计将导入 {{count}} 条记录",
|
229
|
+
totalTables: '{{count}} 个表',
|
230
|
+
},
|
220
231
|
information: '社区与资讯',
|
221
232
|
installPWA: '安装浏览器应用 (PWA)',
|
222
233
|
lang: {
|
@@ -16,6 +16,16 @@ export default {
|
|
16
16
|
detail: '错误详情',
|
17
17
|
title: '请求失败',
|
18
18
|
},
|
19
|
+
import: {
|
20
|
+
importConfigFile: {
|
21
|
+
description: '出错原因: {{reason}}',
|
22
|
+
title: '导入失败',
|
23
|
+
},
|
24
|
+
incompatible: {
|
25
|
+
description: '该文件由更高版本导出,请尝试升级到最新版本后再重新导入',
|
26
|
+
title: '当前应用不支持导入该文件',
|
27
|
+
},
|
28
|
+
},
|
19
29
|
loginRequired: {
|
20
30
|
desc: '即将自动跳转到登录页面',
|
21
31
|
title: '请登录后使用该功能',
|