diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..1a1e213
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,5 @@
+{
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 180
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index d79ffeb..936e486 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,10 @@
"@codemirror/merge": "^6.9.0",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.36.0",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@kintone/rest-api-client": "^6.1.2",
diff --git a/package.json b/package.json
index dbe68d8..2eb3888 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,10 @@
"@codemirror/merge": "^6.9.0",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.36.0",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@kintone/rest-api-client": "^6.1.2",
diff --git a/src/renderer/src/components/AppDetail/AppDetail.tsx b/src/renderer/src/components/AppDetail/AppDetail.tsx
index f953ef1..63b5755 100644
--- a/src/renderer/src/components/AppDetail/AppDetail.tsx
+++ b/src/renderer/src/components/AppDetail/AppDetail.tsx
@@ -342,11 +342,8 @@ const AppDetail: React.FC = () => {
);
}
- const changeCount =
- currentDomain && selectedAppId
- ? fileChangeStore.getChangeCount(currentDomain.id, selectedAppId)
- : { added: 0, deleted: 0 };
- const hasChanges = changeCount.added > 0 || changeCount.deleted > 0;
+ const changeCount = currentDomain && selectedAppId ? fileChangeStore.getChangeCount(currentDomain.id, selectedAppId) : { added: 0, deleted: 0, reordered: 0 };
+ const hasChanges = changeCount.added > 0 || changeCount.deleted > 0 || changeCount.reordered > 0;
return (
@@ -394,8 +391,10 @@ const AppDetail: React.FC = () => {
}}
>
{changeCount.added > 0 && `+${changeCount.added}`}
- {changeCount.added > 0 && changeCount.deleted > 0 && " "}
+ {changeCount.added > 0 && (changeCount.deleted > 0 || changeCount.reordered > 0) && " "}
{changeCount.deleted > 0 && `-${changeCount.deleted}`}
+ {(changeCount.added > 0 || changeCount.deleted > 0) && changeCount.reordered > 0 && " "}
+ {changeCount.reordered > 0 && `~${changeCount.reordered}`}
)}
diff --git a/src/renderer/src/components/AppDetail/FileItem.tsx b/src/renderer/src/components/AppDetail/FileItem.tsx
index a7c0b87..751519e 100644
--- a/src/renderer/src/components/AppDetail/FileItem.tsx
+++ b/src/renderer/src/components/AppDetail/FileItem.tsx
@@ -1,6 +1,6 @@
/**
* FileItem Component
- * Displays a single file with status indicator (unchanged/added/deleted).
+ * Displays a single file with status indicator (unchanged/added/deleted/reordered).
* Shows appropriate action buttons based on status.
*/
@@ -84,69 +84,78 @@ const FileItem: React.FC
= ({
const { styles, cx } = useStyles();
const token = useTheme();
- const statusColor = {
+ const statusColor: Record = {
unchanged: "transparent",
added: token.colorSuccess,
deleted: token.colorError,
- }[entry.status];
+ reordered: token.colorWarning,
+ };
return (
-
-
-
-
- {entry.status !== "unchanged" && (
-
- )}
-
+
+
+ {entry.status !== "unchanged" && (
+
-
+
+ {entry.fileName}
+
+ {entry.status === "added" && (
+
+ )}
+ {entry.status === "deleted" && (
+
+ )}
+ {entry.status === "reordered" && (
+
+ )}
+
+
+
+ {entry.size && (
+ {formatFileSize(entry.size)}
+ )}
+
+ {entry.status === "deleted" ? (
+ }
+ onClick={onRestore}
>
- {entry.fileName}
-
- {entry.status === "added" && (
-
- )}
- {entry.status === "deleted" && (
-
- )}
-
-
-
- {entry.size && (
- {formatFileSize(entry.size)}
- )}
-
- {entry.status === "deleted" ? (
- }
- onClick={onRestore}
- >
- {t("restore")}
-
- ) : (
- <>
- {entry.status === "unchanged" && onView && entry.fileKey && (
+ {t("restore")}
+
+ ) : (
+ <>
+ {(entry.status === "unchanged" || entry.status === "reordered") &&
+ onView &&
+ entry.fileKey && (
)}
- {entry.status === "unchanged" && onDownload && entry.fileKey && (
+ {(entry.status === "unchanged" || entry.status === "reordered") &&
+ onDownload &&
+ entry.fileKey && (
)}
- }
- onClick={onDelete}
- />
- >
- )}
-
-
-
+ }
+ onClick={onDelete}
+ />
+ >
+ )}
+
+
);
};
-export default FileItem;
+export default FileItem;
\ No newline at end of file
diff --git a/src/renderer/src/components/AppDetail/FileSection.tsx b/src/renderer/src/components/AppDetail/FileSection.tsx
index 043f4ae..4a18eab 100644
--- a/src/renderer/src/components/AppDetail/FileSection.tsx
+++ b/src/renderer/src/components/AppDetail/FileSection.tsx
@@ -7,12 +7,12 @@
import React, { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tag, App as AntApp } from "antd";
-import { SortableList } from "@lobehub/ui";
import { createStyles, useTheme } from "antd-style";
import { useFileChangeStore } from "@renderer/stores";
import type { FileEntry } from "@renderer/stores";
import FileItem from "./FileItem";
import DropZone from "./DropZone";
+import SortableFileList from "./SortableFileList";
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
@@ -86,13 +86,6 @@ const useStyles = createStyles(({ token, css }) => ({
pointer-events: none;
z-index: 10;
`,
- sortableItem: css`
- background: ${token.colorBgContainer};
-
- &:hover {
- background: ${token.colorBgTextHover};
- }
- `,
}));
const FileSection: React.FC = ({
@@ -123,7 +116,8 @@ const FileSection: React.FC = ({
const addedCount = files.filter((f) => f.status === "added").length;
const deletedCount = files.filter((f) => f.status === "deleted").length;
- const hasChanges = addedCount > 0 || deletedCount > 0;
+ const reorderedCount = files.filter((f) => f.status === "reordered").length;
+ const hasChanges = addedCount > 0 || deletedCount > 0 || reorderedCount > 0;
// ── Shared save logic ─────────────────────────────────────────────────────
const saveFile = useCallback(
@@ -281,14 +275,15 @@ const FileSection: React.FC = ({
);
// ── Reorder ────────────────────────────────────────────────────────────────
- const handleSortChange = useCallback(
- (newItems: { id: string }[]) => {
+ const handleReorder = useCallback(
+ (newOrder: string[], draggedFileId: string) => {
reorderSection(
domainId,
appId,
platform,
fileType,
- newItems.map((i) => i.id),
+ newOrder,
+ draggedFileId
);
},
[domainId, appId, platform, fileType, reorderSection],
@@ -296,39 +291,32 @@ const FileSection: React.FC = ({
// ── Render item ────────────────────────────────────────────────────────────
const renderItem = useCallback(
- (item: { id: string }) => {
- const entry = files.find((f) => f.id === item.id);
- if (!entry) return null;
-
+ (entry: FileEntry) => {
return (
-
- handleDelete(entry)}
- onRestore={() => handleRestore(entry.id)}
- onView={
- entry.fileKey
- ? () => onView(entry.fileKey!, entry.fileName)
- : undefined
- }
- onDownload={
- entry.fileKey
- ? () => onDownload(entry.fileKey!, entry.fileName)
- : undefined
- }
- isDownloading={downloadingKey === entry.fileKey}
- />
-
+ handleDelete(entry)}
+ onRestore={() => handleRestore(entry.id)}
+ onView={
+ entry.fileKey
+ ? () => onView(entry.fileKey!, entry.fileName)
+ : undefined
+ }
+ onDownload={
+ entry.fileKey
+ ? () => onDownload(entry.fileKey!, entry.fileName)
+ : undefined
+ }
+ isDownloading={downloadingKey === entry.fileKey}
+ />
);
},
[
- files,
handleDelete,
handleRestore,
onView,
onDownload,
downloadingKey,
- styles.sortableItem,
],
);
@@ -348,8 +336,10 @@ const FileSection: React.FC = ({
}}
>
{addedCount > 0 && `+${addedCount}`}
- {addedCount > 0 && deletedCount > 0 && " "}
+ {addedCount > 0 && (deletedCount > 0 || reorderedCount > 0) && " "}
{deletedCount > 0 && `-${deletedCount}`}
+ {(addedCount > 0 || deletedCount > 0) && reorderedCount > 0 && " "}
+ {reorderedCount > 0 && `~${reorderedCount}`}
)}
@@ -388,11 +378,10 @@ const FileSection: React.FC = ({
{files.length === 0 ? (
{t("noConfig")}
) : (
-
)}
diff --git a/src/renderer/src/components/AppDetail/SortableFileList.tsx b/src/renderer/src/components/AppDetail/SortableFileList.tsx
new file mode 100644
index 0000000..3e4242c
--- /dev/null
+++ b/src/renderer/src/components/AppDetail/SortableFileList.tsx
@@ -0,0 +1,75 @@
+/**
+ * SortableFileList Component
+ * A sortable list component using dnd-kit for drag-and-drop reordering.
+ * Provides precise tracking of which item was dragged.
+ * Uses LobeHub UI's SortableList.Item and SortableList.DragHandle for consistent styling.
+ */
+
+import React, { useCallback } from "react";
+import { SortableList } from "@lobehub/ui";
+import {
+ DndContext,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragEndEvent,
+} from "@dnd-kit/core";
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+ arrayMove,
+ sortableKeyboardCoordinates,
+} from "@dnd-kit/sortable";
+import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers";
+import type { FileEntry } from "@renderer/stores";
+
+// ── SortableFileList Component ────────────────────────────────────────────────
+interface SortableFileListProps {
+ items: FileEntry[];
+ onReorder: (newOrder: string[], draggedItemId: string) => void;
+ renderItem: (entry: FileEntry) => React.ReactNode;
+}
+
+const SortableFileList: React.FC = ({ items, onReorder, renderItem }) => {
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
+ const handleDragEnd = useCallback(
+ (event: DragEndEvent) => {
+ const { active, over } = event;
+
+ if (over && active.id !== over.id) {
+ const oldIndex = items.findIndex((f) => f.id === active.id);
+ const newIndex = items.findIndex((f) => f.id === over.id);
+ const newOrder = arrayMove(items, oldIndex, newIndex).map((f) => f.id);
+
+ onReorder(newOrder, active.id as string);
+ }
+ },
+ [items, onReorder]
+ );
+
+ return (
+
+
+ {items.map((entry) => (
+
+ {renderItem(entry)}
+
+ ))}
+
+
+ );
+};
+
+export default SortableFileList;
\ No newline at end of file
diff --git a/src/renderer/src/locales/en-US/app.json b/src/renderer/src/locales/en-US/app.json
index 1ce00bb..d1819a4 100644
--- a/src/renderer/src/locales/en-US/app.json
+++ b/src/renderer/src/locales/en-US/app.json
@@ -47,5 +47,6 @@
"fileDeleteFailed": "Failed to delete file",
"statusAdded": "New",
"statusDeleted": "Deleted",
+ "statusReordered": "Moved",
"restore": "Restore"
}
diff --git a/src/renderer/src/locales/ja-JP/app.json b/src/renderer/src/locales/ja-JP/app.json
index eee3d64..d0a9a67 100644
--- a/src/renderer/src/locales/ja-JP/app.json
+++ b/src/renderer/src/locales/ja-JP/app.json
@@ -47,5 +47,6 @@
"fileDeleteFailed": "ファイルの削除に失敗しました",
"statusAdded": "新規",
"statusDeleted": "削除",
+ "statusReordered": "順序変更",
"restore": "復元"
}
diff --git a/src/renderer/src/locales/zh-CN/app.json b/src/renderer/src/locales/zh-CN/app.json
index 375a979..97fc8fd 100644
--- a/src/renderer/src/locales/zh-CN/app.json
+++ b/src/renderer/src/locales/zh-CN/app.json
@@ -47,5 +47,6 @@
"fileDeleteFailed": "删除文件失败",
"statusAdded": "新增",
"statusDeleted": "删除",
+ "statusReordered": "已移动",
"restore": "恢复"
}
diff --git a/src/renderer/src/stores/fileChangeStore.ts b/src/renderer/src/stores/fileChangeStore.ts
index 70414fc..05c5066 100644
--- a/src/renderer/src/stores/fileChangeStore.ts
+++ b/src/renderer/src/stores/fileChangeStore.ts
@@ -1,6 +1,6 @@
/**
* File Change Store
- * Manages file change state (added/deleted/unchanged) per app.
+ * Manages file change state (added/deleted/unchanged/reordered) per app.
* File content is NOT stored here — only metadata and local disk paths.
* State is persisted so pending changes survive app restarts.
*/
@@ -29,6 +29,8 @@ export interface FileEntry {
interface AppFileState {
files: FileEntry[];
initialized: boolean;
+ /** Original order for each section: key is `${platform}:${fileType}`, value is ordered file IDs */
+ originalSectionOrders: Record;
}
interface FileChangeState {
@@ -52,7 +54,7 @@ interface FileChangeState {
/**
* Mark a file for deletion, or remove an added file from the list.
- * - unchanged → deleted
+ * - unchanged/reordered → deleted
* - added → removed from list entirely
* - deleted → no-op
*/
@@ -66,7 +68,7 @@ interface FileChangeState {
/**
* Reorder files within a specific (platform, fileType) section.
- * newOrder is the new ordered array of file IDs for that section.
+ * The dragged file's status will be set to "reordered" (if it was "unchanged").
*/
reorderSection: (
domainId: string,
@@ -74,6 +76,7 @@ interface FileChangeState {
platform: "desktop" | "mobile",
fileType: "js" | "css",
newOrder: string[],
+ draggedFileId: string
) => void;
/**
@@ -93,11 +96,11 @@ interface FileChangeState {
fileType: "js" | "css",
) => FileEntry[];
- /** Count of added and deleted files */
+ /** Count of added, deleted, and reordered files */
getChangeCount: (
domainId: string,
- appId: string,
- ) => { added: number; deleted: number };
+ appId: string
+ ) => { added: number; deleted: number; reordered: number };
isInitialized: (domainId: string, appId: string) => boolean;
}
@@ -119,10 +122,20 @@ export const useFileChangeStore = create()(
status: "unchanged" as FileStatus,
}));
+ // Build original section orders
+ const originalSectionOrders: Record = {};
+ for (const f of entries) {
+ const sectionKey = `${f.platform}:${f.fileType}`;
+ if (!originalSectionOrders[sectionKey]) {
+ originalSectionOrders[sectionKey] = [];
+ }
+ originalSectionOrders[sectionKey].push(f.id);
+ }
+
set((state) => ({
appFiles: {
...state.appFiles,
- [key]: { files: entries, initialized: true },
+ [key]: { files: entries, initialized: true, originalSectionOrders },
},
}));
},
@@ -130,7 +143,11 @@ export const useFileChangeStore = create()(
addFile: (domainId, appId, entry) => {
const key = appKey(domainId, appId);
set((state) => {
- const existing = state.appFiles[key] ?? { files: [], initialized: true };
+ const existing = state.appFiles[key] ?? {
+ files: [],
+ initialized: true,
+ originalSectionOrders: {},
+ };
return {
appFiles: {
...state.appFiles,
@@ -156,8 +173,8 @@ export const useFileChangeStore = create()(
if (file.status === "added") {
// Remove added files entirely
updatedFiles = existing.files.filter((f) => f.id !== fileId);
- } else if (file.status === "unchanged") {
- // Mark unchanged files as deleted
+ } else if (file.status === "unchanged" || file.status === "reordered") {
+ // Mark unchanged/reordered files as deleted
updatedFiles = existing.files.map((f) =>
f.id === fileId ? { ...f, status: "deleted" as FileStatus } : f,
);
@@ -196,7 +213,7 @@ export const useFileChangeStore = create()(
});
},
- reorderSection: (domainId, appId, platform, fileType, newOrder) => {
+ reorderSection: (domainId, appId, platform, fileType, newOrder, draggedFileId) => {
const key = appKey(domainId, appId);
set((state) => {
const existing = state.appFiles[key];
@@ -221,11 +238,15 @@ export const useFileChangeStore = create()(
if (!newOrder.includes(f.id)) reordered.push(f);
}
- // Merge: maintain overall section order in the flat array
- // Order: all non-section files first (in their original positions),
- // but to maintain section integrity, rebuild in platform/fileType groups.
- // Simple approach: replace section slice with reordered.
- const finalFiles = [...otherFiles, ...reordered];
+ // Mark the dragged file as "reordered" if it was "unchanged"
+ const finalSectionFiles = reordered.map((f) => {
+ if (f.id === draggedFileId && f.status === "unchanged") {
+ return { ...f, status: "reordered" as FileStatus };
+ }
+ return f;
+ });
+
+ const finalFiles = [...otherFiles, ...finalSectionFiles];
return {
appFiles: {
@@ -241,7 +262,11 @@ export const useFileChangeStore = create()(
set((state) => ({
appFiles: {
...state.appFiles,
- [key]: { files: [], initialized: false },
+ [key]: {
+ files: [],
+ initialized: false,
+ originalSectionOrders: {},
+ },
},
}));
},
@@ -262,9 +287,11 @@ export const useFileChangeStore = create()(
getChangeCount: (domainId, appId) => {
const key = appKey(domainId, appId);
const files = get().appFiles[key]?.files ?? [];
+
return {
added: files.filter((f) => f.status === "added").length,
deleted: files.filter((f) => f.status === "deleted").length,
+ reordered: files.filter((f) => f.status === "reordered").length,
};
},
@@ -275,6 +302,6 @@ export const useFileChangeStore = create()(
}),
{
name: "file-change-storage",
- },
- ),
+ }
+ )
);
diff --git a/src/shared/types/ipc.ts b/src/shared/types/ipc.ts
index e53f2ad..fcc02c1 100644
--- a/src/shared/types/ipc.ts
+++ b/src/shared/types/ipc.ts
@@ -66,7 +66,7 @@ export interface DeployFile {
position: string;
}
-export type FileStatus = "unchanged" | "added" | "deleted";
+export type FileStatus = "unchanged" | "added" | "deleted" | "reordered";
export interface DeployFileEntry {
id: string;