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/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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
@@ -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}`}
|
||||
</Tag>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -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<FileItemProps> = ({
|
||||
const { styles, cx } = useStyles();
|
||||
const token = useTheme();
|
||||
|
||||
const statusColor = {
|
||||
const statusColor: Record<string, string> = {
|
||||
unchanged: "transparent",
|
||||
added: token.colorSuccess,
|
||||
deleted: token.colorError,
|
||||
}[entry.status];
|
||||
reordered: token.colorWarning,
|
||||
};
|
||||
|
||||
return (
|
||||
<SortableList.Item id={entry.id}>
|
||||
<div className={styles.item}>
|
||||
<div className={styles.fileInfo}>
|
||||
<SortableList.DragHandle />
|
||||
{entry.status !== "unchanged" && (
|
||||
<div
|
||||
className={styles.statusDot}
|
||||
style={{ background: statusColor }}
|
||||
/>
|
||||
)}
|
||||
<MaterialFileTypeIcon
|
||||
type="file"
|
||||
filename={`file.${entry.fileType}`}
|
||||
size={16}
|
||||
<div className={styles.item}>
|
||||
<div className={styles.fileInfo}>
|
||||
<SortableList.DragHandle />
|
||||
{entry.status !== "unchanged" && (
|
||||
<div
|
||||
className={styles.statusDot}
|
||||
style={{ background: statusColor[entry.status] }}
|
||||
/>
|
||||
<span
|
||||
className={cx(
|
||||
styles.fileName,
|
||||
entry.status === "deleted" && styles.fileNameDeleted,
|
||||
)}
|
||||
)}
|
||||
<MaterialFileTypeIcon
|
||||
type="file"
|
||||
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}
|
||||
</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" }}
|
||||
/>
|
||||
)}
|
||||
</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 && (
|
||||
{t("restore")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{(entry.status === "unchanged" || entry.status === "reordered") &&
|
||||
onView &&
|
||||
entry.fileKey && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@@ -156,7 +165,9 @@ const FileItem: React.FC<FileItemProps> = ({
|
||||
{t("view")}
|
||||
</Button>
|
||||
)}
|
||||
{entry.status === "unchanged" && onDownload && entry.fileKey && (
|
||||
{(entry.status === "unchanged" || entry.status === "reordered") &&
|
||||
onDownload &&
|
||||
entry.fileKey && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@@ -167,19 +178,18 @@ const FileItem: React.FC<FileItemProps> = ({
|
||||
{t("download", { ns: "common" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<Trash2 size={16} />}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</SortableList.Item>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<Trash2 size={16} />}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileItem;
|
||||
export default FileItem;
|
||||
@@ -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<FileSectionProps> = ({
|
||||
@@ -123,7 +116,8 @@ const FileSection: React.FC<FileSectionProps> = ({
|
||||
|
||||
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<FileSectionProps> = ({
|
||||
);
|
||||
|
||||
// ── 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<FileSectionProps> = ({
|
||||
|
||||
// ── Render item ────────────────────────────────────────────────────────────
|
||||
const renderItem = useCallback(
|
||||
(item: { id: string }) => {
|
||||
const entry = files.find((f) => f.id === item.id);
|
||||
if (!entry) return null;
|
||||
|
||||
(entry: FileEntry) => {
|
||||
return (
|
||||
<div className={styles.sortableItem}>
|
||||
<FileItem
|
||||
entry={entry}
|
||||
onDelete={() => 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}
|
||||
/>
|
||||
</div>
|
||||
<FileItem
|
||||
entry={entry}
|
||||
onDelete={() => 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<FileSectionProps> = ({
|
||||
}}
|
||||
>
|
||||
{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}`}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
@@ -388,11 +378,10 @@ const FileSection: React.FC<FileSectionProps> = ({
|
||||
{files.length === 0 ? (
|
||||
<div className={styles.emptySection}>{t("noConfig")}</div>
|
||||
) : (
|
||||
<SortableList
|
||||
<SortableFileList
|
||||
items={files}
|
||||
onReorder={handleReorder}
|
||||
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",
|
||||
"statusAdded": "New",
|
||||
"statusDeleted": "Deleted",
|
||||
"statusReordered": "Moved",
|
||||
"restore": "Restore"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,6 @@
|
||||
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
||||
"statusAdded": "新規",
|
||||
"statusDeleted": "削除",
|
||||
"statusReordered": "順序変更",
|
||||
"restore": "復元"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,6 @@
|
||||
"fileDeleteFailed": "删除文件失败",
|
||||
"statusAdded": "新增",
|
||||
"statusDeleted": "删除",
|
||||
"statusReordered": "已移动",
|
||||
"restore": "恢复"
|
||||
}
|
||||
|
||||
@@ -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<string, string[]>;
|
||||
}
|
||||
|
||||
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<FileChangeState>()(
|
||||
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) => ({
|
||||
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) => {
|
||||
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<FileChangeState>()(
|
||||
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<FileChangeState>()(
|
||||
});
|
||||
},
|
||||
|
||||
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<FileChangeState>()(
|
||||
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<FileChangeState>()(
|
||||
set((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) => {
|
||||
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<FileChangeState>()(
|
||||
}),
|
||||
{
|
||||
name: "file-change-storage",
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user