feat: add provider duplicate functionality in edit mode
Add a duplicate button next to the drag handle in edit mode that allows users to quickly copy existing provider configurations. - Add Copy icon button in ProviderCard next to drag handle - Implement handleDuplicateProvider in App.tsx with deep cloning - Copy all provider settings (settingsConfig, websiteUrl, category, meta) - Auto-generate new ID and timestamp, omit sortIndex for natural sorting - Append " copy" to duplicated provider name - Add i18n support (zh: "复制", en: "Duplicate") - Wire onDuplicate callback through ProviderList to ProviderCard The duplicated provider will appear below the original provider in the list, sorted by creation time.
This commit is contained in:
21
src/App.tsx
21
src/App.tsx
@@ -59,7 +59,7 @@ function App() {
|
|||||||
if (event.appType === activeApp) {
|
if (event.appType === activeApp) {
|
||||||
await refetch();
|
await refetch();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[App] Failed to subscribe provider switch event", error);
|
console.error("[App] Failed to subscribe provider switch event", error);
|
||||||
@@ -99,6 +99,22 @@ function App() {
|
|||||||
setConfirmDelete(null);
|
setConfirmDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 复制供应商
|
||||||
|
const handleDuplicateProvider = async (provider: Provider) => {
|
||||||
|
const duplicatedProvider: Omit<Provider, "id" | "createdAt"> = {
|
||||||
|
name: `${provider.name} copy`,
|
||||||
|
settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝
|
||||||
|
websiteUrl: provider.websiteUrl,
|
||||||
|
category: provider.category,
|
||||||
|
meta: provider.meta
|
||||||
|
? JSON.parse(JSON.stringify(provider.meta))
|
||||||
|
: undefined, // 深拷贝
|
||||||
|
// sortIndex 不复制,让它按 createdAt 自然排序
|
||||||
|
};
|
||||||
|
|
||||||
|
await addProvider(duplicatedProvider);
|
||||||
|
};
|
||||||
|
|
||||||
// 导入配置成功后刷新
|
// 导入配置成功后刷新
|
||||||
const handleImportSuccess = async () => {
|
const handleImportSuccess = async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -136,7 +152,7 @@ function App() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setIsEditMode(!isEditMode)}
|
onClick={() => setIsEditMode(!isEditMode)}
|
||||||
title={t(
|
title={t(
|
||||||
isEditMode ? "header.exitEditMode" : "header.enterEditMode"
|
isEditMode ? "header.exitEditMode" : "header.enterEditMode",
|
||||||
)}
|
)}
|
||||||
className={
|
className={
|
||||||
isEditMode
|
isEditMode
|
||||||
@@ -177,6 +193,7 @@ function App() {
|
|||||||
onSwitch={switchProvider}
|
onSwitch={switchProvider}
|
||||||
onEdit={setEditingProvider}
|
onEdit={setEditingProvider}
|
||||||
onDelete={setConfirmDelete}
|
onDelete={setConfirmDelete}
|
||||||
|
onDuplicate={handleDuplicateProvider}
|
||||||
onConfigureUsage={setUsageProvider}
|
onConfigureUsage={setUsageProvider}
|
||||||
onOpenWebsite={handleOpenWebsite}
|
onOpenWebsite={handleOpenWebsite}
|
||||||
onCreate={() => setIsAddOpen(true)}
|
onCreate={() => setIsAddOpen(true)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { MoveVertical } from "lucide-react";
|
import { MoveVertical, Copy } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type {
|
import type {
|
||||||
DraggableAttributes,
|
DraggableAttributes,
|
||||||
@@ -27,6 +27,7 @@ interface ProviderCardProps {
|
|||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
onConfigureUsage: (provider: Provider) => void;
|
onConfigureUsage: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
|
onDuplicate: (provider: Provider) => void;
|
||||||
dragHandleProps?: DragHandleProps;
|
dragHandleProps?: DragHandleProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export function ProviderCard({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
|
onDuplicate,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
}: ProviderCardProps) {
|
}: ProviderCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -107,7 +109,8 @@ export function ProviderCard({
|
|||||||
isEditMode
|
isEditMode
|
||||||
? "w-8 mr-3 border-muted hover:border-border-hover hover:text-foreground hover:bg-muted/50 opacity-100"
|
? "w-8 mr-3 border-muted hover:border-border-hover hover:text-foreground hover:bg-muted/50 opacity-100"
|
||||||
: "w-0 mr-0 border-transparent opacity-0 pointer-events-none",
|
: "w-0 mr-0 border-transparent opacity-0 pointer-events-none",
|
||||||
dragHandleProps?.isDragging && "border-border-active text-primary bg-primary/10 cursor-grabbing",
|
dragHandleProps?.isDragging &&
|
||||||
|
"border-border-active text-primary bg-primary/10 cursor-grabbing",
|
||||||
)}
|
)}
|
||||||
aria-label={t("provider.dragHandle")}
|
aria-label={t("provider.dragHandle")}
|
||||||
{...(dragHandleProps?.attributes ?? {})}
|
{...(dragHandleProps?.attributes ?? {})}
|
||||||
@@ -116,6 +119,21 @@ export function ProviderCard({
|
|||||||
<MoveVertical className="h-4 w-4" />
|
<MoveVertical className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"mt-1 flex h-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-md border text-muted-foreground transition-all duration-200",
|
||||||
|
isEditMode
|
||||||
|
? "w-8 mr-3 border-muted hover:border-border-hover hover:text-foreground hover:bg-muted/50 opacity-100 cursor-pointer"
|
||||||
|
: "w-0 mr-0 border-transparent opacity-0 pointer-events-none",
|
||||||
|
)}
|
||||||
|
onClick={() => onDuplicate(provider)}
|
||||||
|
aria-label={t("provider.duplicate")}
|
||||||
|
title={t("provider.duplicate")}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
||||||
<h3 className="text-base font-semibold leading-none">
|
<h3 className="text-base font-semibold leading-none">
|
||||||
@@ -124,7 +142,7 @@ export function ProviderCard({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400 transition-opacity duration-200",
|
"rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400 transition-opacity duration-200",
|
||||||
isCurrent ? "opacity-100" : "opacity-0 pointer-events-none"
|
isCurrent ? "opacity-100" : "opacity-0 pointer-events-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t("provider.currentlyUsing")}
|
{t("provider.currentlyUsing")}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface ProviderListProps {
|
|||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
@@ -34,6 +35,7 @@ export function ProviderList({
|
|||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
onCreate,
|
onCreate,
|
||||||
@@ -82,6 +84,7 @@ export function ProviderList({
|
|||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
onConfigureUsage={onConfigureUsage}
|
onConfigureUsage={onConfigureUsage}
|
||||||
onOpenWebsite={onOpenWebsite}
|
onOpenWebsite={onOpenWebsite}
|
||||||
/>
|
/>
|
||||||
@@ -100,6 +103,7 @@ interface SortableProviderCardProps {
|
|||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
}
|
}
|
||||||
@@ -112,6 +116,7 @@ function SortableProviderCard({
|
|||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
}: SortableProviderCardProps) {
|
}: SortableProviderCardProps) {
|
||||||
@@ -139,6 +144,7 @@ function SortableProviderCard({
|
|||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
onConfigureUsage={
|
onConfigureUsage={
|
||||||
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"removeFromClaudePlugin": "Remove from Claude plugin",
|
"removeFromClaudePlugin": "Remove from Claude plugin",
|
||||||
"dragToReorder": "Drag to reorder",
|
"dragToReorder": "Drag to reorder",
|
||||||
"dragHandle": "Drag to reorder",
|
"dragHandle": "Drag to reorder",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
"sortUpdateFailed": "Failed to update sort order",
|
"sortUpdateFailed": "Failed to update sort order",
|
||||||
"configureUsage": "Configure usage query",
|
"configureUsage": "Configure usage query",
|
||||||
"name": "Provider Name",
|
"name": "Provider Name",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"removeFromClaudePlugin": "从 Claude 插件移除",
|
"removeFromClaudePlugin": "从 Claude 插件移除",
|
||||||
"dragToReorder": "拖拽以重新排序",
|
"dragToReorder": "拖拽以重新排序",
|
||||||
"dragHandle": "拖拽排序",
|
"dragHandle": "拖拽排序",
|
||||||
|
"duplicate": "复制",
|
||||||
"sortUpdateFailed": "排序更新失败",
|
"sortUpdateFailed": "排序更新失败",
|
||||||
"configureUsage": "配置用量查询",
|
"configureUsage": "配置用量查询",
|
||||||
"name": "供应商名称",
|
"name": "供应商名称",
|
||||||
|
|||||||
Reference in New Issue
Block a user