优化快捷回复 CSV 为中文四列模板,操作列改为单行
导入/导出统一为一级分类、二级分类、输入码、内容;列表操作按钮紧凑并排不换行。
This commit is contained in:
@@ -451,6 +451,53 @@ func (h *QuickReplyHandler) Use(c *gin.Context) {
|
|||||||
middleware.JSON(c, gin.H{"ok": true, "usage_count": item.UsageCount + 1})
|
middleware.JSON(c, gin.H{"ok": true, "usage_count": item.UsageCount + 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 快捷回复 CSV 固定四列(与导入模板一致)
|
||||||
|
// 一级分类=团队/个人;二级分类=标题;输入码=快捷键;内容=回复正文
|
||||||
|
func quickReplyCSVHeader() []string {
|
||||||
|
return []string{"一级分类", "二级分类", "输入码", "内容"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopeLabelCN(scope string) string {
|
||||||
|
if scope == quickReplyScopeTeam {
|
||||||
|
return "团队"
|
||||||
|
}
|
||||||
|
return "个人"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseQuickReplyScopeLabel 仅接受 团队 / 个人(及空=沿用默认)
|
||||||
|
func parseQuickReplyScopeLabel(raw string) (string, bool) {
|
||||||
|
s := strings.TrimSpace(raw)
|
||||||
|
s = strings.TrimPrefix(s, "\ufeff")
|
||||||
|
switch s {
|
||||||
|
case "团队":
|
||||||
|
return quickReplyScopeTeam, true
|
||||||
|
case "个人":
|
||||||
|
return quickReplyScopePersonal, true
|
||||||
|
case "":
|
||||||
|
return "", true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellAt(row []string, idx int) string {
|
||||||
|
if idx < 0 || idx >= len(row) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(row[idx], "\ufeff"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isQuickReplyCSVHeader 判断首行是否为标准表头
|
||||||
|
func isQuickReplyCSVHeader(row []string) bool {
|
||||||
|
if len(row) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cellAt(row, 0) == "一级分类" &&
|
||||||
|
cellAt(row, 1) == "二级分类" &&
|
||||||
|
cellAt(row, 2) == "输入码" &&
|
||||||
|
cellAt(row, 3) == "内容"
|
||||||
|
}
|
||||||
|
|
||||||
// Export GET /api/quick-replies/export?scope=team|personal
|
// Export GET /api/quick-replies/export?scope=team|personal
|
||||||
func (h *QuickReplyHandler) Export(c *gin.Context) {
|
func (h *QuickReplyHandler) Export(c *gin.Context) {
|
||||||
tenantID := middleware.GetTenantID(c)
|
tenantID := middleware.GetTenantID(c)
|
||||||
@@ -480,51 +527,53 @@ func (h *QuickReplyHandler) Export(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
header := []string{"title", "content", "shortcut", "group_name", "status", "scope"}
|
header := quickReplyCSVHeader()
|
||||||
rows := make([][]string, 0, len(list))
|
rows := make([][]string, 0, len(list))
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
|
scopeLabelCN(item.Scope),
|
||||||
item.Title,
|
item.Title,
|
||||||
item.Content,
|
|
||||||
item.Shortcut,
|
item.Shortcut,
|
||||||
item.GroupName,
|
item.Content,
|
||||||
item.Status,
|
|
||||||
item.Scope,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
name := fmt.Sprintf("quick_replies_%s_%s.csv", scope, time.Now().Format("20060102_150405"))
|
name := fmt.Sprintf("快捷回复_%s_%s.csv", scopeLabelCN(scope), time.Now().Format("20060102_150405"))
|
||||||
writeCSVResponse(c, name, header, rows)
|
writeCSVResponse(c, name, header, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportTemplate GET /api/quick-replies/import-template
|
// ImportTemplate GET /api/quick-replies/import-template
|
||||||
func (h *QuickReplyHandler) ImportTemplate(c *gin.Context) {
|
func (h *QuickReplyHandler) ImportTemplate(c *gin.Context) {
|
||||||
header := []string{"title", "content", "shortcut", "group_name", "status"}
|
header := quickReplyCSVHeader()
|
||||||
rows := [][]string{
|
rows := [][]string{
|
||||||
{"打招呼", "您好!请问有什么可以帮您?", "nh", "通用", "published"},
|
{"团队", "欢迎词", "hy", "欢迎访问,请问有什么可以帮您?"},
|
||||||
{"稍等", "好的,请稍等,我帮您查询一下。", "sd", "通用", "published"},
|
{"个人", "您好", "nh", "您好!很高兴为您服务。"},
|
||||||
|
{"团队", "稍等", "sd", "好的,请稍等,我帮您查询一下。"},
|
||||||
}
|
}
|
||||||
writeCSVResponse(c, "quick_replies_template.csv", header, rows)
|
writeCSVResponse(c, "快捷回复导入模板.csv", header, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import POST /api/quick-replies/import multipart file + scope=team|personal + on_conflict=skip|overwrite
|
// Import POST /api/quick-replies/import
|
||||||
|
// 固定 CSV 列:一级分类,二级分类,输入码,内容
|
||||||
|
// 一级分类为空时使用表单 scope;可在同一文件中混写团队/个人。
|
||||||
func (h *QuickReplyHandler) Import(c *gin.Context) {
|
func (h *QuickReplyHandler) Import(c *gin.Context) {
|
||||||
tenantID := middleware.GetTenantID(c)
|
tenantID := middleware.GetTenantID(c)
|
||||||
uid := middleware.GetUserID(c)
|
uid := middleware.GetUserID(c)
|
||||||
scope := strings.TrimSpace(c.DefaultPostForm("scope", c.Query("scope")))
|
defaultScope := strings.TrimSpace(c.DefaultPostForm("scope", c.Query("scope")))
|
||||||
if scope == "" {
|
if defaultScope == "" {
|
||||||
scope = quickReplyScopePersonal
|
defaultScope = quickReplyScopePersonal
|
||||||
}
|
}
|
||||||
if scope != quickReplyScopeTeam && scope != quickReplyScopePersonal {
|
if defaultScope != quickReplyScopeTeam && defaultScope != quickReplyScopePersonal {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "scope 无效"})
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "scope 无效"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if scope == quickReplyScopeTeam && !requireTeamQuickReplyManager(c) {
|
if defaultScope == quickReplyScopeTeam && !requireTeamQuickReplyManager(c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onConflict := strings.TrimSpace(c.DefaultPostForm("on_conflict", "skip"))
|
onConflict := strings.TrimSpace(c.DefaultPostForm("on_conflict", "skip"))
|
||||||
if onConflict != "skip" && onConflict != "overwrite" {
|
if onConflict != "skip" && onConflict != "overwrite" {
|
||||||
onConflict = "skip"
|
onConflict = "skip"
|
||||||
}
|
}
|
||||||
|
canManageTeam := middleware.HasAnyRole(c, "admin", "supervisor")
|
||||||
|
|
||||||
file, _, err := c.Request.FormFile("file")
|
file, _, err := c.Request.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -546,10 +595,16 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳过表头
|
|
||||||
start := 0
|
start := 0
|
||||||
if len(records[0]) > 0 && strings.EqualFold(strings.TrimSpace(records[0][0]), "title") {
|
if isQuickReplyCSVHeader(records[0]) {
|
||||||
start = 1
|
start = 1
|
||||||
|
} else {
|
||||||
|
// 必须使用标准模板表头
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "CSV 表头不正确,请下载「导入模板」:一级分类,二级分类,输入码,内容",
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
body := records[start:]
|
body := records[start:]
|
||||||
if len(body) > maxQuickReplyImport {
|
if len(body) > maxQuickReplyImport {
|
||||||
@@ -557,11 +612,6 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownerID *uint
|
|
||||||
if scope == quickReplyScopePersonal {
|
|
||||||
ownerID = &uid
|
|
||||||
}
|
|
||||||
|
|
||||||
created, updated, skipped := 0, 0, 0
|
created, updated, skipped := 0, 0, 0
|
||||||
var errors []string
|
var errors []string
|
||||||
|
|
||||||
@@ -570,19 +620,36 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
if len(row) == 0 || (len(row) == 1 && strings.TrimSpace(row[0]) == "") {
|
if len(row) == 0 || (len(row) == 1 && strings.TrimSpace(row[0]) == "") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// pad columns
|
// 固定列:0一级分类 1二级分类 2输入码 3内容
|
||||||
for len(row) < 5 {
|
for len(row) < 4 {
|
||||||
row = append(row, "")
|
row = append(row, "")
|
||||||
}
|
}
|
||||||
title := strings.TrimSpace(row[0])
|
|
||||||
content := strings.TrimSpace(row[1])
|
rawScope := cellAt(row, 0)
|
||||||
shortcutRaw := strings.TrimSpace(row[2])
|
title := cellAt(row, 1)
|
||||||
group := strings.TrimSpace(row[3])
|
shortcutRaw := cellAt(row, 2)
|
||||||
status := normalizeQRStatus(row[4], quickReplyStatusPub)
|
content := cellAt(row, 3)
|
||||||
|
|
||||||
|
rowScope := defaultScope
|
||||||
|
parsed, ok := parseQuickReplyScopeLabel(rawScope)
|
||||||
|
if !ok {
|
||||||
|
skipped++
|
||||||
|
errors = append(errors, fmt.Sprintf("第 %d 行:一级分类请填「团队」或「个人」", lineNo))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parsed != "" {
|
||||||
|
rowScope = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowScope == quickReplyScopeTeam && !canManageTeam {
|
||||||
|
skipped++
|
||||||
|
errors = append(errors, fmt.Sprintf("第 %d 行:无权限导入团队快捷回复", lineNo))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if title == "" || content == "" {
|
if title == "" || content == "" {
|
||||||
skipped++
|
skipped++
|
||||||
errors = append(errors, fmt.Sprintf("第 %d 行:标题或内容为空", lineNo))
|
errors = append(errors, fmt.Sprintf("第 %d 行:二级分类或内容为空", lineNo))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if utf8.RuneCountInString(title) > maxQuickReplyTitle || utf8.RuneCountInString(content) > maxQuickReplyContent {
|
if utf8.RuneCountInString(title) > maxQuickReplyTitle || utf8.RuneCountInString(content) > maxQuickReplyContent {
|
||||||
@@ -596,24 +663,26 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
errors = append(errors, fmt.Sprintf("第 %d 行:%s", lineNo, err.Error()))
|
errors = append(errors, fmt.Sprintf("第 %d 行:%s", lineNo, err.Error()))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if utf8.RuneCountInString(group) > maxQuickReplyGroup {
|
|
||||||
group = string([]rune(group)[:maxQuickReplyGroup])
|
var ownerID *uint
|
||||||
|
if rowScope == quickReplyScopePersonal {
|
||||||
|
ownerID = &uid
|
||||||
}
|
}
|
||||||
|
|
||||||
// 冲突:有 shortcut 按 shortcut;否则按 title
|
// 冲突:有 shortcut 按 shortcut;否则按 title
|
||||||
var existing model.QuickReply
|
var existing model.QuickReply
|
||||||
found := false
|
found := false
|
||||||
if shortcut != "" {
|
if shortcut != "" {
|
||||||
q := model.DB.Where("tenant_id = ? AND scope = ? AND shortcut = ?", tenantID, scope, shortcut)
|
q := model.DB.Where("tenant_id = ? AND scope = ? AND shortcut = ?", tenantID, rowScope, shortcut)
|
||||||
if scope == quickReplyScopePersonal {
|
if rowScope == quickReplyScopePersonal {
|
||||||
q = q.Where("owner_user_id = ?", uid)
|
q = q.Where("owner_user_id = ?", uid)
|
||||||
}
|
}
|
||||||
if err := q.First(&existing).Error; err == nil {
|
if err := q.First(&existing).Error; err == nil {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
q := model.DB.Where("tenant_id = ? AND scope = ? AND title = ?", tenantID, scope, title)
|
q := model.DB.Where("tenant_id = ? AND scope = ? AND title = ?", tenantID, rowScope, title)
|
||||||
if scope == quickReplyScopePersonal {
|
if rowScope == quickReplyScopePersonal {
|
||||||
q = q.Where("owner_user_id = ?", uid)
|
q = q.Where("owner_user_id = ?", uid)
|
||||||
}
|
}
|
||||||
if err := q.First(&existing).Error; err == nil {
|
if err := q.First(&existing).Error; err == nil {
|
||||||
@@ -628,7 +697,6 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := model.DB.Model(&existing).Updates(map[string]interface{}{
|
if err := model.DB.Model(&existing).Updates(map[string]interface{}{
|
||||||
"title": title, "content": content, "shortcut": shortcut,
|
"title": title, "content": content, "shortcut": shortcut,
|
||||||
"group_name": group, "status": status,
|
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
skipped++
|
skipped++
|
||||||
errors = append(errors, fmt.Sprintf("第 %d 行:更新失败", lineNo))
|
errors = append(errors, fmt.Sprintf("第 %d 行:更新失败", lineNo))
|
||||||
@@ -640,13 +708,12 @@ func (h *QuickReplyHandler) Import(c *gin.Context) {
|
|||||||
|
|
||||||
item := model.QuickReply{
|
item := model.QuickReply{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
Scope: scope,
|
Scope: rowScope,
|
||||||
OwnerUserID: ownerID,
|
OwnerUserID: ownerID,
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Shortcut: shortcut,
|
Shortcut: shortcut,
|
||||||
GroupName: group,
|
Status: quickReplyStatusPub,
|
||||||
Status: status,
|
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&item).Error; err != nil {
|
if err := model.DB.Create(&item).Error; err != nil {
|
||||||
skipped++
|
skipped++
|
||||||
|
|||||||
@@ -32,3 +32,33 @@ func TestNormalizeShortcut(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseQuickReplyScopeLabel(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{"团队", "team", true},
|
||||||
|
{"个人", "personal", true},
|
||||||
|
{"", "", true},
|
||||||
|
{"team", "", false},
|
||||||
|
{"personal", "", false},
|
||||||
|
{"其它", "", false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got, ok := parseQuickReplyScopeLabel(tc.in)
|
||||||
|
if ok != tc.wantOK || got != tc.want {
|
||||||
|
t.Fatalf("parseQuickReplyScopeLabel(%q)=(%q,%v) want (%q,%v)", tc.in, got, ok, tc.want, tc.wantOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsQuickReplyCSVHeader(t *testing.T) {
|
||||||
|
if !isQuickReplyCSVHeader([]string{"一级分类", "二级分类", "输入码", "内容"}) {
|
||||||
|
t.Fatal("expected header match")
|
||||||
|
}
|
||||||
|
if isQuickReplyCSVHeader([]string{"title", "content", "shortcut", "group_name"}) {
|
||||||
|
t.Fatal("legacy english header should not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import {
|
import {
|
||||||
PlusOutlined, EditOutlined, DeleteOutlined, CloudUploadOutlined, DownloadOutlined,
|
PlusOutlined, CloudUploadOutlined, DownloadOutlined,
|
||||||
ThunderboltOutlined, CheckOutlined, StopOutlined, ImportOutlined, ExportOutlined,
|
ThunderboltOutlined, ImportOutlined, ExportOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
createQuickReply, deleteQuickReply, downloadQuickReplyTemplate, exportQuickReplies,
|
createQuickReply, deleteQuickReply, downloadQuickReplyTemplate, exportQuickReplies,
|
||||||
@@ -210,33 +210,33 @@ const QuickReplies = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: tab === 'team' && canManageTeam ? 220 : 120,
|
width: tab === 'team' && canManageTeam ? 168 : 112,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={4} wrap>
|
<div className="flex items-center gap-0.5 whitespace-nowrap">
|
||||||
{canEditRow(row) && (
|
{canEditRow(row) && (
|
||||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(row)}>
|
<Button type="link" size="small" className="!px-1" onClick={() => openEdit(row)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{tab === 'team' && canManageTeam && row.status === 'draft' && (
|
{tab === 'team' && canManageTeam && row.status === 'draft' && (
|
||||||
<Button type="link" size="small" icon={<CheckOutlined />} onClick={() => void handlePublish(row)}>
|
<Button type="link" size="small" className="!px-1" onClick={() => void handlePublish(row)}>
|
||||||
发布同步
|
发布
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{tab === 'team' && canManageTeam && row.status === 'published' && (
|
{tab === 'team' && canManageTeam && row.status === 'published' && (
|
||||||
<Button type="link" size="small" icon={<StopOutlined />} onClick={() => void handleUnpublish(row)}>
|
<Button type="link" size="small" className="!px-1" onClick={() => void handleUnpublish(row)}>
|
||||||
下线
|
下线
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canEditRow(row) && (
|
{canEditRow(row) && (
|
||||||
<Popconfirm title="确认删除?" onConfirm={() => void handleDelete(row)}>
|
<Popconfirm title="确认删除?" onConfirm={() => void handleDelete(row)}>
|
||||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
<Button type="link" size="small" danger className="!px-1">
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -272,16 +272,21 @@ const QuickReplies = () => {
|
|||||||
<h1 className="text-base font-semibold text-neutral-900 m-0 truncate">快捷回复</h1>
|
<h1 className="text-base font-semibold text-neutral-900 m-0 truncate">快捷回复</h1>
|
||||||
</div>
|
</div>
|
||||||
<Space wrap size="small" className="shrink-0">
|
<Space wrap size="small" className="shrink-0">
|
||||||
<Button size="small" icon={<DownloadOutlined />} onClick={() => void handleTemplate()}>
|
<Button size="small" icon={<DownloadOutlined />} onClick={() => void handleTemplate()} title="下载 CSV 模板(一级分类/二级分类/输入码/内容)">
|
||||||
导入模板
|
导入模板
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" icon={<ExportOutlined />} onClick={() => void handleExport()}>
|
<Button size="small" icon={<ExportOutlined />} onClick={() => void handleExport()}>
|
||||||
导出
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
<Upload
|
<Upload
|
||||||
accept=".csv,text/csv"
|
accept=".csv,text/csv,.xlsx,.xls"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
beforeUpload={async file => {
|
beforeUpload={async file => {
|
||||||
|
const name = (file as File).name || ''
|
||||||
|
if (/\.xlsx?$/i.test(name)) {
|
||||||
|
message.warning('请先将 Excel 另存为 CSV(UTF-8)后再导入;可先下载「导入模板」参考格式')
|
||||||
|
return false
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await importQuickReplies(file as File, effectiveTab, 'skip')
|
const res = await importQuickReplies(file as File, effectiveTab, 'skip')
|
||||||
const d = res.data
|
const d = res.data
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ export const exportQuickReplies = (scope: QuickReplyScope) =>
|
|||||||
downloadFile(`/quick-replies/export?scope=${scope}`, `quick_replies_${scope}.csv`)
|
downloadFile(`/quick-replies/export?scope=${scope}`, `quick_replies_${scope}.csv`)
|
||||||
|
|
||||||
export const downloadQuickReplyTemplate = () =>
|
export const downloadQuickReplyTemplate = () =>
|
||||||
downloadFile('/quick-replies/import-template', 'quick_replies_template.csv')
|
downloadFile('/quick-replies/import-template', '快捷回复导入模板.csv')
|
||||||
|
|
||||||
export const importQuickReplies = (file: File, scope: QuickReplyScope, onConflict: 'skip' | 'overwrite' = 'skip') => {
|
export const importQuickReplies = (file: File, scope: QuickReplyScope, onConflict: 'skip' | 'overwrite' = 'skip') => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
|||||||
Reference in New Issue
Block a user