add order hint
This commit is contained in:
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 180
|
||||||
|
}
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -14,6 +14,10 @@
|
|||||||
"@codemirror/merge": "^6.9.0",
|
"@codemirror/merge": "^6.9.0",
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"@codemirror/view": "^6.36.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/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@kintone/rest-api-client": "^6.1.2",
|
"@kintone/rest-api-client": "^6.1.2",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
"@codemirror/merge": "^6.9.0",
|
"@codemirror/merge": "^6.9.0",
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"@codemirror/view": "^6.36.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/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@kintone/rest-api-client": "^6.1.2",
|
"@kintone/rest-api-client": "^6.1.2",
|
||||||
|
|||||||
@@ -342,11 +342,8 @@ const AppDetail: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeCount =
|
const changeCount = currentDomain && selectedAppId ? fileChangeStore.getChangeCount(currentDomain.id, selectedAppId) : { added: 0, deleted: 0, reordered: 0 };
|
||||||
currentDomain && selectedAppId
|
const hasChanges = changeCount.added > 0 || changeCount.deleted > 0 || changeCount.reordered > 0;
|
||||||
? fileChangeStore.getChangeCount(currentDomain.id, selectedAppId)
|
|
||||||
: { added: 0, deleted: 0 };
|
|
||||||
const hasChanges = changeCount.added > 0 || changeCount.deleted > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -394,8 +391,10 @@ const AppDetail: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{changeCount.added > 0 && `+${changeCount.added}`}
|
{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.deleted > 0 && `-${changeCount.deleted}`}
|
||||||
|
{(changeCount.added > 0 || changeCount.deleted > 0) && changeCount.reordered > 0 && " "}
|
||||||
|
{changeCount.reordered > 0 && `~${changeCount.reordered}`}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* FileItem Component
|
* 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.
|
* Shows appropriate action buttons based on status.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -84,69 +84,78 @@ const FileItem: React.FC<FileItemProps> = ({
|
|||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const token = useTheme();
|
const token = useTheme();
|
||||||
|
|
||||||
const statusColor = {
|
const statusColor: Record<string, string> = {
|
||||||
unchanged: "transparent",
|
unchanged: "transparent",
|
||||||
added: token.colorSuccess,
|
added: token.colorSuccess,
|
||||||
deleted: token.colorError,
|
deleted: token.colorError,
|
||||||
}[entry.status];
|
reordered: token.colorWarning,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableList.Item id={entry.id}>
|
<div className={styles.item}>
|
||||||
<div className={styles.item}>
|
<div className={styles.fileInfo}>
|
||||||
<div className={styles.fileInfo}>
|
<SortableList.DragHandle />
|
||||||
<SortableList.DragHandle />
|
{entry.status !== "unchanged" && (
|
||||||
{entry.status !== "unchanged" && (
|
<div
|
||||||
<div
|
className={styles.statusDot}
|
||||||
className={styles.statusDot}
|
style={{ background: statusColor[entry.status] }}
|
||||||
style={{ background: statusColor }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MaterialFileTypeIcon
|
|
||||||
type="file"
|
|
||||||
filename={`file.${entry.fileType}`}
|
|
||||||
size={16}
|
|
||||||
/>
|
/>
|
||||||
<span
|
)}
|
||||||
className={cx(
|
<MaterialFileTypeIcon
|
||||||
styles.fileName,
|
type="file"
|
||||||
entry.status === "deleted" && styles.fileNameDeleted,
|
filename={`file.${entry.fileType}`}
|
||||||
)}
|
size={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
styles.fileName,
|
||||||
|
entry.status === "deleted" && styles.fileNameDeleted,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{entry.fileName}
|
||||||
|
</span>
|
||||||
|
{entry.status === "added" && (
|
||||||
|
<Badge
|
||||||
|
color={token.colorSuccess}
|
||||||
|
text={t("statusAdded")}
|
||||||
|
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{entry.status === "deleted" && (
|
||||||
|
<Badge
|
||||||
|
color={token.colorError}
|
||||||
|
text={t("statusDeleted")}
|
||||||
|
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{entry.status === "reordered" && (
|
||||||
|
<Badge
|
||||||
|
color={token.colorWarning}
|
||||||
|
text={t("statusReordered")}
|
||||||
|
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
{entry.size && (
|
||||||
|
<span className={styles.fileSize}>{formatFileSize(entry.size)}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.status === "deleted" ? (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<Undo2 size={16} />}
|
||||||
|
onClick={onRestore}
|
||||||
>
|
>
|
||||||
{entry.fileName}
|
{t("restore")}
|
||||||
</span>
|
</Button>
|
||||||
{entry.status === "added" && (
|
) : (
|
||||||
<Badge
|
<>
|
||||||
color={token.colorSuccess}
|
{(entry.status === "unchanged" || entry.status === "reordered") &&
|
||||||
text={t("statusAdded")}
|
onView &&
|
||||||
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
|
entry.fileKey && (
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{entry.status === "deleted" && (
|
|
||||||
<Badge
|
|
||||||
color={token.colorError}
|
|
||||||
text={t("statusDeleted")}
|
|
||||||
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
{entry.size && (
|
|
||||||
<span className={styles.fileSize}>{formatFileSize(entry.size)}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{entry.status === "deleted" ? (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<Undo2 size={16} />}
|
|
||||||
onClick={onRestore}
|
|
||||||
>
|
|
||||||
{t("restore")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{entry.status === "unchanged" && onView && entry.fileKey && (
|
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -156,7 +165,9 @@ const FileItem: React.FC<FileItemProps> = ({
|
|||||||
{t("view")}
|
{t("view")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{entry.status === "unchanged" && onDownload && entry.fileKey && (
|
{(entry.status === "unchanged" || entry.status === "reordered") &&
|
||||||
|
onDownload &&
|
||||||
|
entry.fileKey && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -167,18 +178,17 @@ const FileItem: React.FC<FileItemProps> = ({
|
|||||||
{t("download", { ns: "common" })}
|
{t("download", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
icon={<Trash2 size={16} />}
|
icon={<Trash2 size={16} />}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</SortableList.Item>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Tag, App as AntApp } from "antd";
|
import { Tag, App as AntApp } from "antd";
|
||||||
import { SortableList } from "@lobehub/ui";
|
|
||||||
import { createStyles, useTheme } from "antd-style";
|
import { createStyles, useTheme } from "antd-style";
|
||||||
import { useFileChangeStore } from "@renderer/stores";
|
import { useFileChangeStore } from "@renderer/stores";
|
||||||
import type { FileEntry } from "@renderer/stores";
|
import type { FileEntry } from "@renderer/stores";
|
||||||
import FileItem from "./FileItem";
|
import FileItem from "./FileItem";
|
||||||
import DropZone from "./DropZone";
|
import DropZone from "./DropZone";
|
||||||
|
import SortableFileList from "./SortableFileList";
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
|
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
|
||||||
|
|
||||||
@@ -86,13 +86,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
`,
|
`,
|
||||||
sortableItem: css`
|
|
||||||
background: ${token.colorBgContainer};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${token.colorBgTextHover};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FileSection: React.FC<FileSectionProps> = ({
|
const FileSection: React.FC<FileSectionProps> = ({
|
||||||
@@ -123,7 +116,8 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
|
|
||||||
const addedCount = files.filter((f) => f.status === "added").length;
|
const addedCount = files.filter((f) => f.status === "added").length;
|
||||||
const deletedCount = files.filter((f) => f.status === "deleted").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 ─────────────────────────────────────────────────────
|
// ── Shared save logic ─────────────────────────────────────────────────────
|
||||||
const saveFile = useCallback(
|
const saveFile = useCallback(
|
||||||
@@ -281,14 +275,15 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ── Reorder ────────────────────────────────────────────────────────────────
|
// ── Reorder ────────────────────────────────────────────────────────────────
|
||||||
const handleSortChange = useCallback(
|
const handleReorder = useCallback(
|
||||||
(newItems: { id: string }[]) => {
|
(newOrder: string[], draggedFileId: string) => {
|
||||||
reorderSection(
|
reorderSection(
|
||||||
domainId,
|
domainId,
|
||||||
appId,
|
appId,
|
||||||
platform,
|
platform,
|
||||||
fileType,
|
fileType,
|
||||||
newItems.map((i) => i.id),
|
newOrder,
|
||||||
|
draggedFileId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[domainId, appId, platform, fileType, reorderSection],
|
[domainId, appId, platform, fileType, reorderSection],
|
||||||
@@ -296,39 +291,32 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
|
|
||||||
// ── Render item ────────────────────────────────────────────────────────────
|
// ── Render item ────────────────────────────────────────────────────────────
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(item: { id: string }) => {
|
(entry: FileEntry) => {
|
||||||
const entry = files.find((f) => f.id === item.id);
|
|
||||||
if (!entry) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.sortableItem}>
|
<FileItem
|
||||||
<FileItem
|
entry={entry}
|
||||||
entry={entry}
|
onDelete={() => handleDelete(entry)}
|
||||||
onDelete={() => handleDelete(entry)}
|
onRestore={() => handleRestore(entry.id)}
|
||||||
onRestore={() => handleRestore(entry.id)}
|
onView={
|
||||||
onView={
|
entry.fileKey
|
||||||
entry.fileKey
|
? () => onView(entry.fileKey!, entry.fileName)
|
||||||
? () => onView(entry.fileKey!, entry.fileName)
|
: undefined
|
||||||
: undefined
|
}
|
||||||
}
|
onDownload={
|
||||||
onDownload={
|
entry.fileKey
|
||||||
entry.fileKey
|
? () => onDownload(entry.fileKey!, entry.fileName)
|
||||||
? () => onDownload(entry.fileKey!, entry.fileName)
|
: undefined
|
||||||
: undefined
|
}
|
||||||
}
|
isDownloading={downloadingKey === entry.fileKey}
|
||||||
isDownloading={downloadingKey === entry.fileKey}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
files,
|
|
||||||
handleDelete,
|
handleDelete,
|
||||||
handleRestore,
|
handleRestore,
|
||||||
onView,
|
onView,
|
||||||
onDownload,
|
onDownload,
|
||||||
downloadingKey,
|
downloadingKey,
|
||||||
styles.sortableItem,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -348,8 +336,10 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{addedCount > 0 && `+${addedCount}`}
|
{addedCount > 0 && `+${addedCount}`}
|
||||||
{addedCount > 0 && deletedCount > 0 && " "}
|
{addedCount > 0 && (deletedCount > 0 || reorderedCount > 0) && " "}
|
||||||
{deletedCount > 0 && `-${deletedCount}`}
|
{deletedCount > 0 && `-${deletedCount}`}
|
||||||
|
{(addedCount > 0 || deletedCount > 0) && reorderedCount > 0 && " "}
|
||||||
|
{reorderedCount > 0 && `~${reorderedCount}`}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -388,11 +378,10 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className={styles.emptySection}>{t("noConfig")}</div>
|
<div className={styles.emptySection}>{t("noConfig")}</div>
|
||||||
) : (
|
) : (
|
||||||
<SortableList
|
<SortableFileList
|
||||||
items={files}
|
items={files}
|
||||||
|
onReorder={handleReorder}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
onChange={handleSortChange}
|
|
||||||
gap={0}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
75
src/renderer/src/components/AppDetail/SortableFileList.tsx
Normal file
75
src/renderer/src/components/AppDetail/SortableFileList.tsx
Normal file
@@ -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<SortableFileListProps> = ({ 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 (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={undefined}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
|
||||||
|
>
|
||||||
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
|
{items.map((entry) => (
|
||||||
|
<SortableList.Item key={entry.id} id={entry.id}>
|
||||||
|
{renderItem(entry)}
|
||||||
|
</SortableList.Item>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableFileList;
|
||||||
@@ -47,5 +47,6 @@
|
|||||||
"fileDeleteFailed": "Failed to delete file",
|
"fileDeleteFailed": "Failed to delete file",
|
||||||
"statusAdded": "New",
|
"statusAdded": "New",
|
||||||
"statusDeleted": "Deleted",
|
"statusDeleted": "Deleted",
|
||||||
|
"statusReordered": "Moved",
|
||||||
"restore": "Restore"
|
"restore": "Restore"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,6 @@
|
|||||||
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
||||||
"statusAdded": "新規",
|
"statusAdded": "新規",
|
||||||
"statusDeleted": "削除",
|
"statusDeleted": "削除",
|
||||||
|
"statusReordered": "順序変更",
|
||||||
"restore": "復元"
|
"restore": "復元"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,6 @@
|
|||||||
"fileDeleteFailed": "删除文件失败",
|
"fileDeleteFailed": "删除文件失败",
|
||||||
"statusAdded": "新增",
|
"statusAdded": "新增",
|
||||||
"statusDeleted": "删除",
|
"statusDeleted": "删除",
|
||||||
|
"statusReordered": "已移动",
|
||||||
"restore": "恢复"
|
"restore": "恢复"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* File Change Store
|
* 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.
|
* File content is NOT stored here — only metadata and local disk paths.
|
||||||
* State is persisted so pending changes survive app restarts.
|
* State is persisted so pending changes survive app restarts.
|
||||||
*/
|
*/
|
||||||
@@ -29,6 +29,8 @@ export interface FileEntry {
|
|||||||
interface AppFileState {
|
interface AppFileState {
|
||||||
files: FileEntry[];
|
files: FileEntry[];
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
|
/** Original order for each section: key is `${platform}:${fileType}`, value is ordered file IDs */
|
||||||
|
originalSectionOrders: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileChangeState {
|
interface FileChangeState {
|
||||||
@@ -52,7 +54,7 @@ interface FileChangeState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a file for deletion, or remove an added file from the list.
|
* Mark a file for deletion, or remove an added file from the list.
|
||||||
* - unchanged → deleted
|
* - unchanged/reordered → deleted
|
||||||
* - added → removed from list entirely
|
* - added → removed from list entirely
|
||||||
* - deleted → no-op
|
* - deleted → no-op
|
||||||
*/
|
*/
|
||||||
@@ -66,7 +68,7 @@ interface FileChangeState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorder files within a specific (platform, fileType) section.
|
* 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: (
|
reorderSection: (
|
||||||
domainId: string,
|
domainId: string,
|
||||||
@@ -74,6 +76,7 @@ interface FileChangeState {
|
|||||||
platform: "desktop" | "mobile",
|
platform: "desktop" | "mobile",
|
||||||
fileType: "js" | "css",
|
fileType: "js" | "css",
|
||||||
newOrder: string[],
|
newOrder: string[],
|
||||||
|
draggedFileId: string
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,11 +96,11 @@ interface FileChangeState {
|
|||||||
fileType: "js" | "css",
|
fileType: "js" | "css",
|
||||||
) => FileEntry[];
|
) => FileEntry[];
|
||||||
|
|
||||||
/** Count of added and deleted files */
|
/** Count of added, deleted, and reordered files */
|
||||||
getChangeCount: (
|
getChangeCount: (
|
||||||
domainId: string,
|
domainId: string,
|
||||||
appId: string,
|
appId: string
|
||||||
) => { added: number; deleted: number };
|
) => { added: number; deleted: number; reordered: number };
|
||||||
|
|
||||||
isInitialized: (domainId: string, appId: string) => boolean;
|
isInitialized: (domainId: string, appId: string) => boolean;
|
||||||
}
|
}
|
||||||
@@ -119,10 +122,20 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
status: "unchanged" as FileStatus,
|
status: "unchanged" as FileStatus,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Build original section orders
|
||||||
|
const originalSectionOrders: Record<string, string[]> = {};
|
||||||
|
for (const f of entries) {
|
||||||
|
const sectionKey = `${f.platform}:${f.fileType}`;
|
||||||
|
if (!originalSectionOrders[sectionKey]) {
|
||||||
|
originalSectionOrders[sectionKey] = [];
|
||||||
|
}
|
||||||
|
originalSectionOrders[sectionKey].push(f.id);
|
||||||
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
appFiles: {
|
appFiles: {
|
||||||
...state.appFiles,
|
...state.appFiles,
|
||||||
[key]: { files: entries, initialized: true },
|
[key]: { files: entries, initialized: true, originalSectionOrders },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -130,7 +143,11 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
addFile: (domainId, appId, entry) => {
|
addFile: (domainId, appId, entry) => {
|
||||||
const key = appKey(domainId, appId);
|
const key = appKey(domainId, appId);
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const existing = state.appFiles[key] ?? { files: [], initialized: true };
|
const existing = state.appFiles[key] ?? {
|
||||||
|
files: [],
|
||||||
|
initialized: true,
|
||||||
|
originalSectionOrders: {},
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
appFiles: {
|
appFiles: {
|
||||||
...state.appFiles,
|
...state.appFiles,
|
||||||
@@ -156,8 +173,8 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
if (file.status === "added") {
|
if (file.status === "added") {
|
||||||
// Remove added files entirely
|
// Remove added files entirely
|
||||||
updatedFiles = existing.files.filter((f) => f.id !== fileId);
|
updatedFiles = existing.files.filter((f) => f.id !== fileId);
|
||||||
} else if (file.status === "unchanged") {
|
} else if (file.status === "unchanged" || file.status === "reordered") {
|
||||||
// Mark unchanged files as deleted
|
// Mark unchanged/reordered files as deleted
|
||||||
updatedFiles = existing.files.map((f) =>
|
updatedFiles = existing.files.map((f) =>
|
||||||
f.id === fileId ? { ...f, status: "deleted" as FileStatus } : f,
|
f.id === fileId ? { ...f, status: "deleted" as FileStatus } : f,
|
||||||
);
|
);
|
||||||
@@ -196,7 +213,7 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reorderSection: (domainId, appId, platform, fileType, newOrder) => {
|
reorderSection: (domainId, appId, platform, fileType, newOrder, draggedFileId) => {
|
||||||
const key = appKey(domainId, appId);
|
const key = appKey(domainId, appId);
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const existing = state.appFiles[key];
|
const existing = state.appFiles[key];
|
||||||
@@ -221,11 +238,15 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
if (!newOrder.includes(f.id)) reordered.push(f);
|
if (!newOrder.includes(f.id)) reordered.push(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge: maintain overall section order in the flat array
|
// Mark the dragged file as "reordered" if it was "unchanged"
|
||||||
// Order: all non-section files first (in their original positions),
|
const finalSectionFiles = reordered.map((f) => {
|
||||||
// but to maintain section integrity, rebuild in platform/fileType groups.
|
if (f.id === draggedFileId && f.status === "unchanged") {
|
||||||
// Simple approach: replace section slice with reordered.
|
return { ...f, status: "reordered" as FileStatus };
|
||||||
const finalFiles = [...otherFiles, ...reordered];
|
}
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalFiles = [...otherFiles, ...finalSectionFiles];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appFiles: {
|
appFiles: {
|
||||||
@@ -241,7 +262,11 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
appFiles: {
|
appFiles: {
|
||||||
...state.appFiles,
|
...state.appFiles,
|
||||||
[key]: { files: [], initialized: false },
|
[key]: {
|
||||||
|
files: [],
|
||||||
|
initialized: false,
|
||||||
|
originalSectionOrders: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -262,9 +287,11 @@ export const useFileChangeStore = create<FileChangeState>()(
|
|||||||
getChangeCount: (domainId, appId) => {
|
getChangeCount: (domainId, appId) => {
|
||||||
const key = appKey(domainId, appId);
|
const key = appKey(domainId, appId);
|
||||||
const files = get().appFiles[key]?.files ?? [];
|
const files = get().appFiles[key]?.files ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
added: files.filter((f) => f.status === "added").length,
|
added: files.filter((f) => f.status === "added").length,
|
||||||
deleted: files.filter((f) => f.status === "deleted").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<FileChangeState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "file-change-storage",
|
name: "file-change-storage",
|
||||||
},
|
}
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface DeployFile {
|
|||||||
position: string;
|
position: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileStatus = "unchanged" | "added" | "deleted";
|
export type FileStatus = "unchanged" | "added" | "deleted" | "reordered";
|
||||||
|
|
||||||
export interface DeployFileEntry {
|
export interface DeployFileEntry {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user