@lobehub/chat 1.76.1 → 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.
Files changed (129) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/common.json +12 -1
  4. package/locales/ar/error.json +10 -0
  5. package/locales/ar/models.json +9 -6
  6. package/locales/ar/setting.json +28 -0
  7. package/locales/bg-BG/common.json +12 -1
  8. package/locales/bg-BG/error.json +10 -0
  9. package/locales/bg-BG/models.json +9 -6
  10. package/locales/bg-BG/setting.json +28 -0
  11. package/locales/de-DE/common.json +12 -1
  12. package/locales/de-DE/error.json +10 -0
  13. package/locales/de-DE/models.json +9 -6
  14. package/locales/de-DE/setting.json +28 -0
  15. package/locales/en-US/common.json +12 -1
  16. package/locales/en-US/error.json +10 -0
  17. package/locales/en-US/models.json +9 -6
  18. package/locales/en-US/setting.json +28 -0
  19. package/locales/es-ES/common.json +12 -1
  20. package/locales/es-ES/error.json +10 -0
  21. package/locales/es-ES/models.json +9 -6
  22. package/locales/es-ES/setting.json +28 -0
  23. package/locales/fa-IR/common.json +12 -1
  24. package/locales/fa-IR/error.json +10 -0
  25. package/locales/fa-IR/models.json +9 -6
  26. package/locales/fa-IR/setting.json +28 -0
  27. package/locales/fr-FR/common.json +12 -1
  28. package/locales/fr-FR/error.json +10 -0
  29. package/locales/fr-FR/models.json +9 -6
  30. package/locales/fr-FR/setting.json +28 -0
  31. package/locales/it-IT/common.json +12 -1
  32. package/locales/it-IT/error.json +10 -0
  33. package/locales/it-IT/models.json +9 -6
  34. package/locales/it-IT/setting.json +28 -0
  35. package/locales/ja-JP/common.json +12 -1
  36. package/locales/ja-JP/error.json +10 -0
  37. package/locales/ja-JP/models.json +9 -6
  38. package/locales/ja-JP/setting.json +28 -0
  39. package/locales/ko-KR/common.json +12 -1
  40. package/locales/ko-KR/error.json +10 -0
  41. package/locales/ko-KR/models.json +9 -6
  42. package/locales/ko-KR/setting.json +28 -0
  43. package/locales/nl-NL/common.json +12 -1
  44. package/locales/nl-NL/error.json +10 -0
  45. package/locales/nl-NL/models.json +9 -6
  46. package/locales/nl-NL/setting.json +28 -0
  47. package/locales/pl-PL/common.json +12 -1
  48. package/locales/pl-PL/error.json +10 -0
  49. package/locales/pl-PL/models.json +9 -6
  50. package/locales/pl-PL/setting.json +28 -0
  51. package/locales/pt-BR/common.json +12 -1
  52. package/locales/pt-BR/error.json +10 -0
  53. package/locales/pt-BR/models.json +9 -6
  54. package/locales/pt-BR/setting.json +28 -0
  55. package/locales/ru-RU/common.json +12 -1
  56. package/locales/ru-RU/error.json +10 -0
  57. package/locales/ru-RU/models.json +9 -6
  58. package/locales/ru-RU/setting.json +28 -0
  59. package/locales/tr-TR/common.json +12 -1
  60. package/locales/tr-TR/error.json +10 -0
  61. package/locales/tr-TR/models.json +9 -6
  62. package/locales/tr-TR/setting.json +28 -0
  63. package/locales/vi-VN/common.json +12 -1
  64. package/locales/vi-VN/error.json +10 -0
  65. package/locales/vi-VN/models.json +9 -6
  66. package/locales/vi-VN/setting.json +28 -0
  67. package/locales/zh-CN/common.json +12 -1
  68. package/locales/zh-CN/error.json +10 -0
  69. package/locales/zh-CN/models.json +9 -6
  70. package/locales/zh-CN/setting.json +28 -0
  71. package/locales/zh-TW/common.json +12 -1
  72. package/locales/zh-TW/error.json +10 -0
  73. package/locales/zh-TW/models.json +9 -6
  74. package/locales/zh-TW/setting.json +28 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +1 -1
  77. package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
  78. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
  79. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
  80. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +133 -0
  81. package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
  82. package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
  83. package/src/components/GroupIcon/index.tsx +25 -0
  84. package/src/components/IndexCard/index.tsx +143 -0
  85. package/src/components/ProgressItem/index.tsx +75 -0
  86. package/src/database/repositories/dataExporter/index.test.ts +330 -0
  87. package/src/database/repositories/dataExporter/index.ts +216 -0
  88. package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
  89. package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
  90. package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
  91. package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
  92. package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
  93. package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
  94. package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
  95. package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
  96. package/src/database/repositories/dataImporter/index.ts +684 -289
  97. package/src/features/DataImporter/ImportDetail.tsx +203 -0
  98. package/src/features/DataImporter/SuccessResult.tsx +22 -6
  99. package/src/features/DataImporter/_deprecated.ts +43 -0
  100. package/src/features/DataImporter/config.ts +21 -0
  101. package/src/features/DataImporter/index.tsx +112 -31
  102. package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
  103. package/src/features/User/UserPanel/useMenu.tsx +0 -35
  104. package/src/features/User/__tests__/useMenu.test.tsx +0 -2
  105. package/src/locales/default/common.ts +11 -0
  106. package/src/locales/default/error.ts +10 -0
  107. package/src/locales/default/setting.ts +28 -0
  108. package/src/server/routers/lambda/exporter.ts +25 -0
  109. package/src/server/routers/lambda/importer.ts +19 -3
  110. package/src/server/routers/lambda/index.ts +2 -0
  111. package/src/services/config.ts +80 -135
  112. package/src/services/export/_deprecated.ts +155 -0
  113. package/src/services/export/client.ts +15 -0
  114. package/src/services/export/index.ts +6 -0
  115. package/src/services/export/server.ts +9 -0
  116. package/src/services/export/type.ts +5 -0
  117. package/src/services/import/_deprecated.ts +42 -1
  118. package/src/services/import/client.test.ts +1 -1
  119. package/src/services/import/client.ts +30 -1
  120. package/src/services/import/server.ts +70 -2
  121. package/src/services/import/type.ts +10 -0
  122. package/src/store/global/initialState.ts +1 -0
  123. package/src/types/export.ts +11 -0
  124. package/src/types/exportConfig.ts +2 -0
  125. package/src/types/importer.ts +15 -0
  126. package/src/utils/client/exportFile.ts +21 -0
  127. package/vitest.config.ts +1 -1
  128. package/src/utils/config.ts +0 -109
  129. /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={400}>
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 { ImportResult, ImportResults, configService } from '@/services/config';
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 { ErrorShape, FileUploadState, ImportStage } from '@/types/importer';
15
- import { importConfigFile } from '@/utils/config';
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 [importData, setImportData] = useState<ImportResults | undefined>();
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 (!importData) return;
63
+ if (!importResults) return;
57
64
 
58
- const { type, ...res } = importData;
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: t(`importModal.result.${item as keyof ImportResults}`),
76
+ title: item,
77
+ updated: value.updated || 0,
69
78
  }));
70
- }, [importData]);
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
- setImportData(undefined);
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={40}>
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 ? 500 : 400}
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 importConfigFile(file, async (config) => {
149
- setImportState(ImportStage.Preparing);
158
+ const config = await parseConfigFile(file);
159
+ if (!config) return false;
150
160
 
151
- await configService.importConfigState(config, {
152
- onError: (error) => {
153
- setImportError(error);
154
- },
155
- onFileUploading: (state) => {
156
- setUploadingState(state);
157
- },
158
- onStageChange: (stage) => {
159
- setImportState(stage);
160
- },
161
- onSuccess: (data, duration) => {
162
- if (data) setImportData(data);
163
- setDuration(duration);
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
- await refreshSessions();
168
- await refreshMessages();
169
- await refreshTopics();
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: '请登录后使用该功能',