UI improve
This commit is contained in:
@@ -31,6 +31,8 @@ import type {
|
|||||||
GetAppsParams,
|
GetAppsParams,
|
||||||
GetAppDetailParams,
|
GetAppDetailParams,
|
||||||
GetFileContentParams,
|
GetFileContentParams,
|
||||||
|
GetLocalFileContentParams,
|
||||||
|
LocalFileContent,
|
||||||
DeployParams,
|
DeployParams,
|
||||||
DeployResult,
|
DeployResult,
|
||||||
DownloadParams,
|
DownloadParams,
|
||||||
@@ -288,7 +290,7 @@ function registerGetAppDetail(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get file content
|
* Get file content from Kintone
|
||||||
*/
|
*/
|
||||||
function registerGetFileContent(): void {
|
function registerGetFileContent(): void {
|
||||||
handle<GetFileContentParams, Awaited<ReturnType<KintoneClient["getFileContent"]>>>("getFileContent", async (params) => {
|
handle<GetFileContentParams, Awaited<ReturnType<KintoneClient["getFileContent"]>>>("getFileContent", async (params) => {
|
||||||
@@ -297,6 +299,48 @@ function registerGetFileContent(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local file content from storage
|
||||||
|
*/
|
||||||
|
function registerGetLocalFileContent(): void {
|
||||||
|
handle<GetLocalFileContentParams, LocalFileContent>("getLocalFileContent", async (params) => {
|
||||||
|
const fs = await import("fs");
|
||||||
|
const path = await import("path");
|
||||||
|
|
||||||
|
const { storagePath } = params;
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const buffer = fs.readFileSync(storagePath);
|
||||||
|
|
||||||
|
// Get file name
|
||||||
|
const fileName = path.basename(storagePath);
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
const stats = fs.statSync(storagePath);
|
||||||
|
|
||||||
|
// Determine MIME type from extension
|
||||||
|
const ext = path.extname(storagePath).toLowerCase();
|
||||||
|
let mimeType: string;
|
||||||
|
switch (ext) {
|
||||||
|
case ".js":
|
||||||
|
mimeType = "application/javascript";
|
||||||
|
break;
|
||||||
|
case ".css":
|
||||||
|
mimeType = "text/css";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
size: stats.size,
|
||||||
|
mimeType,
|
||||||
|
content: buffer.toString("base64"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Deploy IPC Handlers ====================
|
// ==================== Deploy IPC Handlers ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -824,6 +868,7 @@ export function registerIpcHandlers(): void {
|
|||||||
registerGetApps();
|
registerGetApps();
|
||||||
registerGetAppDetail();
|
registerGetAppDetail();
|
||||||
registerGetFileContent();
|
registerGetFileContent();
|
||||||
|
registerGetLocalFileContent();
|
||||||
|
|
||||||
// Deploy
|
// Deploy
|
||||||
registerDeploy();
|
registerDeploy();
|
||||||
|
|||||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -7,6 +7,8 @@ import type {
|
|||||||
GetAppsParams,
|
GetAppsParams,
|
||||||
GetAppDetailParams,
|
GetAppDetailParams,
|
||||||
GetFileContentParams,
|
GetFileContentParams,
|
||||||
|
GetLocalFileContentParams,
|
||||||
|
LocalFileContent,
|
||||||
DeployParams,
|
DeployParams,
|
||||||
DeployResult,
|
DeployResult,
|
||||||
DownloadParams,
|
DownloadParams,
|
||||||
@@ -50,6 +52,7 @@ export interface SelfAPI {
|
|||||||
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
||||||
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
||||||
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
|
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
|
||||||
|
getLocalFileContent: (params: GetLocalFileContentParams) => Promise<Result<LocalFileContent>>;
|
||||||
|
|
||||||
// ==================== Deploy ====================
|
// ==================== Deploy ====================
|
||||||
deploy: (params: DeployParams) => Promise<Result<DeployResult>>;
|
deploy: (params: DeployParams) => Promise<Result<DeployResult>>;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const api: SelfAPI = {
|
|||||||
getApps: (params) => ipcRenderer.invoke("getApps", params),
|
getApps: (params) => ipcRenderer.invoke("getApps", params),
|
||||||
getAppDetail: (params) => ipcRenderer.invoke("getAppDetail", params),
|
getAppDetail: (params) => ipcRenderer.invoke("getAppDetail", params),
|
||||||
getFileContent: (params) => ipcRenderer.invoke("getFileContent", params),
|
getFileContent: (params) => ipcRenderer.invoke("getFileContent", params),
|
||||||
|
getLocalFileContent: (params) => ipcRenderer.invoke("getLocalFileContent", params),
|
||||||
|
|
||||||
// Deploy
|
// Deploy
|
||||||
deploy: (params) => ipcRenderer.invoke("deploy", params),
|
deploy: (params) => ipcRenderer.invoke("deploy", params),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* Displays app configuration details with file management and deploy functionality.
|
* Displays app configuration details with file management and deploy functionality.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Spin, Tag, Space, App as AntApp } from "antd";
|
import { Spin, Tag, Space, App as AntApp, Tooltip } from "antd";
|
||||||
import { Button, Empty } from "@lobehub/ui";
|
import { Button, Empty } from "@lobehub/ui";
|
||||||
import { LayoutGrid, Download, History, Rocket, Monitor, Smartphone, ArrowLeft, RefreshCw } from "lucide-react";
|
import { LayoutGrid, Download, History, Rocket, Monitor, Smartphone, ArrowLeft, RefreshCw } from "lucide-react";
|
||||||
import { createStyles, useTheme } from "antd-style";
|
import { createStyles, useTheme } from "antd-style";
|
||||||
@@ -89,6 +89,22 @@ const AppDetail: React.FC = () => {
|
|||||||
const [downloadingAll, setDownloadingAll] = React.useState(false);
|
const [downloadingAll, setDownloadingAll] = React.useState(false);
|
||||||
const [deploying, setDeploying] = React.useState(false);
|
const [deploying, setDeploying] = React.useState(false);
|
||||||
const [refreshing, setRefreshing] = React.useState(false);
|
const [refreshing, setRefreshing] = React.useState(false);
|
||||||
|
const [isAnySectionOverLimit, setIsAnySectionOverLimit] = useState(false);
|
||||||
|
|
||||||
|
// Track over-limit status from each FileSection using refs
|
||||||
|
const overLimitSectionsRef = React.useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const handleOverLimitChange = useCallback(
|
||||||
|
(sectionId: string) => (isOverLimit: boolean) => {
|
||||||
|
if (isOverLimit) {
|
||||||
|
overLimitSectionsRef.current.add(sectionId);
|
||||||
|
} else {
|
||||||
|
overLimitSectionsRef.current.delete(sectionId);
|
||||||
|
}
|
||||||
|
setIsAnySectionOverLimit(overLimitSectionsRef.current.size > 0);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Reset view mode when app changes
|
// Reset view mode when app changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -140,10 +156,10 @@ const AppDetail: React.FC = () => {
|
|||||||
fileChangeStore.initializeApp(currentDomain.id, selectedAppId, files);
|
fileChangeStore.initializeApp(currentDomain.id, selectedAppId, files);
|
||||||
}, [currentApp]);
|
}, [currentApp]);
|
||||||
|
|
||||||
const handleFileClick = useCallback((fileKey: string, name: string) => {
|
const handleFileClick = useCallback((fileKey: string | undefined, name: string, storagePath?: string) => {
|
||||||
const ext = name.split(".").pop()?.toLowerCase();
|
const ext = name.split(".").pop()?.toLowerCase();
|
||||||
const type = ext === "css" ? "css" : "js";
|
const type = ext === "css" ? "css" : "js";
|
||||||
setSelectedFile({ type, fileKey, name });
|
setSelectedFile({ type, fileKey, name, storagePath });
|
||||||
setViewMode("code");
|
setViewMode("code");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -334,27 +350,35 @@ const AppDetail: React.FC = () => {
|
|||||||
<Button icon={<Download size={16} />} loading={downloadingAll} onClick={handleDownloadAll}>
|
<Button icon={<Download size={16} />} loading={downloadingAll} onClick={handleDownloadAll}>
|
||||||
{t("downloadAll")}
|
{t("downloadAll")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" icon={<Rocket size={16} />} loading={deploying} disabled={!hasChanges && !deploying} onClick={handleDeploy}>
|
<Tooltip title={isAnySectionOverLimit ? t("deployDisabledReason") : undefined}>
|
||||||
{t("deploy")}
|
<Button
|
||||||
{hasChanges && (
|
type="primary"
|
||||||
<Tag
|
icon={<Rocket size={16} />}
|
||||||
color="white"
|
loading={deploying}
|
||||||
style={{
|
disabled={(!hasChanges && !deploying) || isAnySectionOverLimit}
|
||||||
color: token.colorPrimary,
|
onClick={handleDeploy}
|
||||||
marginLeft: 4,
|
>
|
||||||
fontSize: token.fontSizeSM,
|
{t("deploy")}
|
||||||
padding: "0 4px",
|
{hasChanges && (
|
||||||
lineHeight: "16px",
|
<Tag
|
||||||
}}
|
color="white"
|
||||||
>
|
style={{
|
||||||
{changeCount.added > 0 && `+${changeCount.added}`}
|
color: token.colorPrimary,
|
||||||
{changeCount.added > 0 && (changeCount.deleted > 0 || changeCount.reordered > 0) && " "}
|
marginLeft: 4,
|
||||||
{changeCount.deleted > 0 && `-${changeCount.deleted}`}
|
fontSize: token.fontSizeSM,
|
||||||
{(changeCount.added > 0 || changeCount.deleted > 0) && changeCount.reordered > 0 && " "}
|
padding: "0 4px",
|
||||||
{changeCount.reordered > 0 && `~${changeCount.reordered}`}
|
lineHeight: "16px",
|
||||||
</Tag>
|
}}
|
||||||
)}
|
>
|
||||||
</Button>
|
{changeCount.added > 0 && `+${changeCount.added}`}
|
||||||
|
{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>
|
||||||
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -371,6 +395,7 @@ const AppDetail: React.FC = () => {
|
|||||||
downloadingKey={downloadingKey}
|
downloadingKey={downloadingKey}
|
||||||
onView={handleFileClick}
|
onView={handleFileClick}
|
||||||
onDownload={handleDownloadFile}
|
onDownload={handleDownloadFile}
|
||||||
|
onOverLimitChange={handleOverLimitChange("desktop-js")}
|
||||||
/>
|
/>
|
||||||
<FileSection
|
<FileSection
|
||||||
title={t("pcCss")}
|
title={t("pcCss")}
|
||||||
@@ -382,6 +407,7 @@ const AppDetail: React.FC = () => {
|
|||||||
downloadingKey={downloadingKey}
|
downloadingKey={downloadingKey}
|
||||||
onView={handleFileClick}
|
onView={handleFileClick}
|
||||||
onDownload={handleDownloadFile}
|
onDownload={handleDownloadFile}
|
||||||
|
onOverLimitChange={handleOverLimitChange("desktop-css")}
|
||||||
/>
|
/>
|
||||||
<FileSection
|
<FileSection
|
||||||
title={t("mobileJs")}
|
title={t("mobileJs")}
|
||||||
@@ -393,6 +419,7 @@ const AppDetail: React.FC = () => {
|
|||||||
downloadingKey={downloadingKey}
|
downloadingKey={downloadingKey}
|
||||||
onView={handleFileClick}
|
onView={handleFileClick}
|
||||||
onDownload={handleDownloadFile}
|
onDownload={handleDownloadFile}
|
||||||
|
onOverLimitChange={handleOverLimitChange("mobile-js")}
|
||||||
/>
|
/>
|
||||||
<FileSection
|
<FileSection
|
||||||
title={t("mobileCss")}
|
title={t("mobileCss")}
|
||||||
@@ -404,6 +431,7 @@ const AppDetail: React.FC = () => {
|
|||||||
downloadingKey={downloadingKey}
|
downloadingKey={downloadingKey}
|
||||||
onView={handleFileClick}
|
onView={handleFileClick}
|
||||||
onDownload={handleDownloadFile}
|
onDownload={handleDownloadFile}
|
||||||
|
onOverLimitChange={handleOverLimitChange("mobile-css")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -411,7 +439,9 @@ const AppDetail: React.FC = () => {
|
|||||||
<Button type="text" icon={<ArrowLeft size={16} />} onClick={handleBackToList} className={styles.backButton}>
|
<Button type="text" icon={<ArrowLeft size={16} />} onClick={handleBackToList} className={styles.backButton}>
|
||||||
{t("backToList")}
|
{t("backToList")}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedFile && <CodeViewer fileKey={selectedFile.fileKey} fileName={selectedFile.name} fileType={selectedFile.type} />}
|
{selectedFile && (
|
||||||
|
<CodeViewer fileKey={selectedFile.fileKey} fileName={selectedFile.name} fileType={selectedFile.type} storagePath={selectedFile.storagePath} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Space, Badge } from "antd";
|
import { Space, Badge, Tooltip } from "antd";
|
||||||
import { Button, MaterialFileTypeIcon, SortableList } from "@lobehub/ui";
|
import { Button, MaterialFileTypeIcon, SortableList } from "@lobehub/ui";
|
||||||
import { Code, Download, Trash2, Undo2 } from "lucide-react";
|
import { Download, Trash2, Undo2 } from "lucide-react";
|
||||||
import { createStyles, useTheme } from "antd-style";
|
import { createStyles, useTheme } from "antd-style";
|
||||||
import type { FileEntry } from "@renderer/stores";
|
import type { FileEntry } from "@renderer/stores";
|
||||||
|
|
||||||
@@ -19,21 +19,31 @@ interface FileItemProps {
|
|||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
|
showDivider?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
|
wrapper: css`
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
item: css`
|
item: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
&:last-child {
|
&:hover {
|
||||||
border-bottom: none;
|
background-color: ${token.colorBgTextHover};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
divider: css`
|
||||||
|
height: 1px;
|
||||||
|
background-color: ${token.colorBorder};
|
||||||
|
margin: 0 ${token.paddingMD}px;
|
||||||
|
`,
|
||||||
fileInfo: css`
|
fileInfo: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -49,7 +59,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
`,
|
`,
|
||||||
fileNameDeleted: css`
|
fileNameDeleted: css`
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: ${token.colorTextDisabled};
|
|
||||||
`,
|
`,
|
||||||
fileSize: css`
|
fileSize: css`
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
@@ -57,12 +66,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
color: ${token.colorTextSecondary};
|
color: ${token.colorTextSecondary};
|
||||||
font-size: ${token.fontSizeSM}px;
|
font-size: ${token.fontSizeSM}px;
|
||||||
`,
|
`,
|
||||||
statusDot: css`
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
`,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const formatFileSize = (size: number | undefined): string => {
|
const formatFileSize = (size: number | undefined): string => {
|
||||||
@@ -72,57 +75,80 @@ const formatFileSize = (size: number | undefined): string => {
|
|||||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileItem: React.FC<FileItemProps> = ({ entry, onDelete, onRestore, onView, onDownload, isDownloading }) => {
|
const FileItem: React.FC<FileItemProps> = ({ entry, onDelete, onRestore, onView, onDownload, isDownloading, showDivider = true }) => {
|
||||||
const { t } = useTranslation(["app", "common"]);
|
const { t } = useTranslation(["app", "common"]);
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const token = useTheme();
|
const token = useTheme();
|
||||||
|
|
||||||
const statusColor: Record<string, string> = {
|
const statusColor: Record<string, string> = {
|
||||||
unchanged: "transparent",
|
|
||||||
added: token.colorSuccess,
|
added: token.colorSuccess,
|
||||||
deleted: token.colorError,
|
deleted: token.colorError,
|
||||||
reordered: token.colorWarning,
|
reordered: token.colorWarning,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleItemClick = () => {
|
||||||
|
// Allow viewing if fileKey (Kintone file) OR storagePath (local file)
|
||||||
|
if (onView && (entry.fileKey || entry.storagePath)) {
|
||||||
|
onView();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragHandleClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = (e: React.MouseEvent, handler: () => void) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handler();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.item}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.fileInfo}>
|
<div className={styles.item} onClick={handleItemClick}>
|
||||||
<SortableList.DragHandle />
|
<div className={styles.fileInfo}>
|
||||||
{entry.status !== "unchanged" && <div className={styles.statusDot} style={{ background: statusColor[entry.status] }} />}
|
<div onClick={handleDragHandleClick}>
|
||||||
<MaterialFileTypeIcon type="file" filename={`file.${entry.fileType}`} size={16} />
|
<SortableList.DragHandle />
|
||||||
<span className={cx(styles.fileName, entry.status === "deleted" && styles.fileNameDeleted)}>{entry.fileName}</span>
|
</div>
|
||||||
{entry.status === "added" && <Badge color={token.colorSuccess} text={t("statusAdded")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />}
|
<MaterialFileTypeIcon type="file" filename={`file.${entry.fileType}`} size={16} />
|
||||||
{entry.status === "deleted" && (
|
<span
|
||||||
<Badge color={token.colorError} text={t("statusDeleted")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
|
className={cx(styles.fileName, entry.status === "deleted" && styles.fileNameDeleted)}
|
||||||
)}
|
style={entry.status !== "unchanged" ? { color: statusColor[entry.status] } : undefined}
|
||||||
{entry.status === "reordered" && (
|
>
|
||||||
<Badge color={token.colorWarning} text={t("statusReordered")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
|
{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={(e) => handleButtonClick(e, onRestore)}>
|
||||||
|
{t("restore")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{(entry.status === "unchanged" || entry.status === "reordered") && onDownload && entry.fileKey && (
|
||||||
|
<Tooltip title={t("downloadFile", { ns: "common" })}>
|
||||||
|
<Button type="text" size="small" icon={<Download size={16} />} loading={isDownloading} onClick={(e) => handleButtonClick(e, onDownload!)} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t("deleteFile", { ns: "common" })}>
|
||||||
|
<Button type="text" size="small" danger icon={<Trash2 size={16} />} onClick={(e) => handleButtonClick(e, onDelete)} />
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
{showDivider && <div className={styles.divider} />}
|
||||||
<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" || entry.status === "reordered") && onView && entry.fileKey && (
|
|
||||||
<Button type="text" size="small" icon={<Code size={16} />} onClick={onView}>
|
|
||||||
{t("view")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{(entry.status === "unchanged" || entry.status === "reordered") && onDownload && entry.fileKey && (
|
|
||||||
<Button type="text" size="small" icon={<Download size={16} />} loading={isDownloading} onClick={onDownload}>
|
|
||||||
{t("download", { ns: "common" })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="text" size="small" danger icon={<Trash2 size={16} />} onClick={onDelete} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
* The entire section is a drag-and-drop target for files.
|
* The entire section is a drag-and-drop target for files.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useEffect, 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 { Alert } 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";
|
||||||
@@ -24,8 +25,9 @@ interface FileSectionProps {
|
|||||||
domainId: string;
|
domainId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
downloadingKey: string | null;
|
downloadingKey: string | null;
|
||||||
onView: (fileKey: string, fileName: string) => void;
|
onView: (fileKey: string | undefined, fileName: string, storagePath?: string) => void;
|
||||||
onDownload: (fileKey: string, fileName: string) => void;
|
onDownload: (fileKey: string, fileName: string) => void;
|
||||||
|
onOverLimitChange?: (isOverLimit: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
@@ -90,7 +92,18 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileType, domainId, appId, downloadingKey, onView, onDownload }) => {
|
const FileSection: React.FC<FileSectionProps> = ({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
platform,
|
||||||
|
fileType,
|
||||||
|
domainId,
|
||||||
|
appId,
|
||||||
|
downloadingKey,
|
||||||
|
onView,
|
||||||
|
onDownload,
|
||||||
|
onOverLimitChange,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const token = useTheme();
|
const token = useTheme();
|
||||||
@@ -104,6 +117,14 @@ const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileTy
|
|||||||
const { getSectionFiles, addFile, deleteFile, restoreFile, reorderSection } = useFileChangeStore();
|
const { getSectionFiles, addFile, deleteFile, restoreFile, reorderSection } = useFileChangeStore();
|
||||||
|
|
||||||
const files = getSectionFiles(domainId, appId, platform, fileType);
|
const files = getSectionFiles(domainId, appId, platform, fileType);
|
||||||
|
// Count files excluding deleted ones for the 30-file limit
|
||||||
|
const fileCount = files.filter((f) => f.status !== "deleted").length;
|
||||||
|
const isOverLimit = fileCount > 30;
|
||||||
|
|
||||||
|
// Notify parent when over-limit state changes
|
||||||
|
useEffect(() => {
|
||||||
|
onOverLimitChange?.(isOverLimit);
|
||||||
|
}, [isOverLimit, onOverLimitChange]);
|
||||||
|
|
||||||
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;
|
||||||
@@ -266,15 +287,20 @@ const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileTy
|
|||||||
|
|
||||||
// ── Render item ────────────────────────────────────────────────────────────
|
// ── Render item ────────────────────────────────────────────────────────────
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(entry: FileEntry) => {
|
(entry: FileEntry, index: number, totalCount: number) => {
|
||||||
|
// Determine if file is viewable (has fileKey for Kintone files OR storagePath for local files)
|
||||||
|
const isViewable = entry.fileKey || entry.storagePath;
|
||||||
|
const isLast = index === totalCount - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileItem
|
<FileItem
|
||||||
entry={entry}
|
entry={entry}
|
||||||
onDelete={() => handleDelete(entry)}
|
onDelete={() => handleDelete(entry)}
|
||||||
onRestore={() => handleRestore(entry.id)}
|
onRestore={() => handleRestore(entry.id)}
|
||||||
onView={entry.fileKey ? () => onView(entry.fileKey!, entry.fileName) : undefined}
|
onView={isViewable ? () => onView(entry.fileKey, entry.fileName, entry.storagePath) : undefined}
|
||||||
onDownload={entry.fileKey ? () => onDownload(entry.fileKey!, entry.fileName) : undefined}
|
onDownload={entry.fileKey ? () => onDownload(entry.fileKey!, entry.fileName) : undefined}
|
||||||
isDownloading={downloadingKey === entry.fileKey}
|
isDownloading={downloadingKey === entry.fileKey}
|
||||||
|
showDivider={!isLast}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -288,23 +314,29 @@ const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileTy
|
|||||||
{icon}
|
{icon}
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Tag
|
<>
|
||||||
color={token.colorPrimary}
|
{addedCount > 0 && (
|
||||||
style={{
|
<Tag color="success" style={{ fontSize: token.fontSizeSM, padding: "0 4px", lineHeight: "16px" }}>
|
||||||
fontSize: token.fontSizeSM,
|
+{addedCount}
|
||||||
padding: "0 4px",
|
</Tag>
|
||||||
lineHeight: "16px",
|
)}
|
||||||
}}
|
{deletedCount > 0 && (
|
||||||
>
|
<Tag color="error" style={{ fontSize: token.fontSizeSM, padding: "0 4px", lineHeight: "16px" }}>
|
||||||
{addedCount > 0 && `+${addedCount}`}
|
-{deletedCount}
|
||||||
{addedCount > 0 && (deletedCount > 0 || reorderedCount > 0) && " "}
|
</Tag>
|
||||||
{deletedCount > 0 && `-${deletedCount}`}
|
)}
|
||||||
{(addedCount > 0 || deletedCount > 0) && reorderedCount > 0 && " "}
|
{reorderedCount > 0 && (
|
||||||
{reorderedCount > 0 && `~${reorderedCount}`}
|
<Tag color="warning" style={{ fontSize: token.fontSizeSM, padding: "0 4px", lineHeight: "16px" }}>
|
||||||
</Tag>
|
~{reorderedCount}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* File limit warning */}
|
||||||
|
{isOverLimit && <Alert type="warning" title={t("fileLimitWarning")} showIcon style={{ marginBottom: token.marginSM }} />}
|
||||||
|
|
||||||
{/* The entire card is the drop target */}
|
{/* The entire card is the drop target */}
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
@@ -343,6 +375,9 @@ const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileTy
|
|||||||
<DropZone fileType={fileType} isSaving={isSaving} onFileSelected={saveFile} />
|
<DropZone fileType={fileType} isSaving={isSaving} onFileSelected={saveFile} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* File limit warning - bottom */}
|
||||||
|
{isOverLimit && <Alert type="warning" title={t("fileLimitWarning")} showIcon style={{ marginTop: token.marginSM }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { FileEntry } from "@renderer/stores";
|
|||||||
interface SortableFileListProps {
|
interface SortableFileListProps {
|
||||||
items: FileEntry[];
|
items: FileEntry[];
|
||||||
onReorder: (newOrder: string[], draggedItemId: string) => void;
|
onReorder: (newOrder: string[], draggedItemId: string) => void;
|
||||||
renderItem: (entry: FileEntry) => React.ReactNode;
|
renderItem: (entry: FileEntry, index: number, totalCount: number) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableFileList: React.FC<SortableFileListProps> = ({ items, onReorder, renderItem }) => {
|
const SortableFileList: React.FC<SortableFileListProps> = ({ items, onReorder, renderItem }) => {
|
||||||
@@ -45,9 +45,9 @@ const SortableFileList: React.FC<SortableFileListProps> = ({ items, onReorder, r
|
|||||||
return (
|
return (
|
||||||
<DndContext sensors={sensors} collisionDetection={undefined} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}>
|
<DndContext sensors={sensors} collisionDetection={undefined} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}>
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items.map((entry) => (
|
{items.map((entry, index) => (
|
||||||
<SortableList.Item key={entry.id} id={entry.id}>
|
<SortableList.Item key={entry.id} id={entry.id}>
|
||||||
{renderItem(entry)}
|
{renderItem(entry, index, items.length)}
|
||||||
</SortableList.Item>
|
</SortableList.Item>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -51,12 +51,14 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
fileKey: string;
|
fileKey?: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
fileType: "js" | "css";
|
fileType: "js" | "css";
|
||||||
|
/** For locally added files: absolute path on disk */
|
||||||
|
storagePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeViewer: React.FC<CodeViewerProps> = ({ fileKey, fileName, fileType }) => {
|
const CodeViewer: React.FC<CodeViewerProps> = ({ fileKey, fileName, fileType, storagePath }) => {
|
||||||
const { t } = useTranslation("file");
|
const { t } = useTranslation("file");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const { appearance } = useTheme();
|
const { appearance } = useTheme();
|
||||||
@@ -71,46 +73,60 @@ const CodeViewer: React.FC<CodeViewerProps> = ({ fileKey, fileName, fileType })
|
|||||||
|
|
||||||
// Load file content
|
// Load file content
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (currentDomain && fileKey) {
|
if (currentDomain && (fileKey || storagePath)) {
|
||||||
loadFileContent();
|
loadFileContent();
|
||||||
}
|
}
|
||||||
}, [currentDomain, fileKey]);
|
}, [currentDomain, fileKey, storagePath]);
|
||||||
|
|
||||||
const loadFileContent = async () => {
|
const loadFileContent = async () => {
|
||||||
if (!currentDomain) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.getFileContent({
|
// Prefer local file (storagePath) for added files
|
||||||
domainId: currentDomain.id,
|
if (storagePath) {
|
||||||
fileKey,
|
const result = await window.api.getLocalFileContent({ storagePath });
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Decode base64 content properly for UTF-8 (including Japanese characters)
|
// Decode base64 content properly for UTF-8
|
||||||
const base64 = result.data.content || "";
|
const base64 = result.data.content || "";
|
||||||
const binaryString = atob(base64);
|
const binaryString = atob(base64);
|
||||||
// Decode as UTF-8 to properly handle Japanese and other multi-byte characters
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
}
|
||||||
}
|
const decoder = new TextDecoder("utf-8");
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoded = decoder.decode(bytes);
|
||||||
const decoded = decoder.decode(bytes);
|
setContent(decoded);
|
||||||
setContent(decoded);
|
detectLanguage();
|
||||||
|
|
||||||
// Detect language from file name
|
|
||||||
if (fileName.endsWith(".css")) {
|
|
||||||
setLanguage("css");
|
|
||||||
} else if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
||||||
setLanguage("js");
|
|
||||||
} else {
|
} else {
|
||||||
setLanguage(fileType);
|
setError(result.error || "Failed to load local file content");
|
||||||
|
}
|
||||||
|
} else if (currentDomain && fileKey) {
|
||||||
|
// Load from Kintone
|
||||||
|
const result = await window.api.getFileContent({
|
||||||
|
domainId: currentDomain.id,
|
||||||
|
fileKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Decode base64 content properly for UTF-8 (including Japanese characters)
|
||||||
|
const base64 = result.data.content || "";
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
// Decode as UTF-8 to properly handle Japanese and other multi-byte characters
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const decoded = decoder.decode(bytes);
|
||||||
|
setContent(decoded);
|
||||||
|
detectLanguage();
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Failed to load file content");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || "Failed to load file content");
|
setError("No file source available");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
setError(err instanceof Error ? err.message : "Unknown error");
|
||||||
@@ -119,6 +135,17 @@ const CodeViewer: React.FC<CodeViewerProps> = ({ fileKey, fileName, fileType })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detectLanguage = () => {
|
||||||
|
// Detect language from file name
|
||||||
|
if (fileName.endsWith(".css")) {
|
||||||
|
setLanguage("css");
|
||||||
|
} else if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
||||||
|
setLanguage("js");
|
||||||
|
} else {
|
||||||
|
setLanguage(fileType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(content);
|
navigator.clipboard.writeText(content);
|
||||||
message.success(t("copiedToClipboard"));
|
message.success(t("copiedToClipboard"));
|
||||||
|
|||||||
@@ -48,5 +48,7 @@
|
|||||||
"statusAdded": "New",
|
"statusAdded": "New",
|
||||||
"statusDeleted": "Deleted",
|
"statusDeleted": "Deleted",
|
||||||
"statusReordered": "Moved",
|
"statusReordered": "Moved",
|
||||||
"restore": "Restore"
|
"restore": "Restore",
|
||||||
|
"fileLimitWarning": "This section has more than 30 files. Please reduce files before deploying.",
|
||||||
|
"deployDisabledReason": "One or more sections exceed the file limit (30)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,7 @@
|
|||||||
"downloadFailed": "Download failed",
|
"downloadFailed": "Download failed",
|
||||||
"downloadAllSuccess": "Downloaded to: {{path}}",
|
"downloadAllSuccess": "Downloaded to: {{path}}",
|
||||||
"refreshSuccess": "Refreshed successfully",
|
"refreshSuccess": "Refreshed successfully",
|
||||||
"refreshFailed": "Refresh failed"
|
"refreshFailed": "Refresh failed",
|
||||||
|
"downloadFile": "Download file",
|
||||||
|
"deleteFile": "Delete file"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,7 @@
|
|||||||
"statusAdded": "新規",
|
"statusAdded": "新規",
|
||||||
"statusDeleted": "削除",
|
"statusDeleted": "削除",
|
||||||
"statusReordered": "順序変更",
|
"statusReordered": "順序変更",
|
||||||
"restore": "復元"
|
"restore": "復元",
|
||||||
|
"fileLimitWarning": "このセクションには30以上のファイルがあります。デプロイ前にファイル数を減らしてください。",
|
||||||
|
"deployDisabledReason": "1つ以上のセクションがファイル制限(30)を超えています"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,7 @@
|
|||||||
"downloadFailed": "ダウンロード失敗",
|
"downloadFailed": "ダウンロード失敗",
|
||||||
"downloadAllSuccess": "ダウンロード先: {{path}}",
|
"downloadAllSuccess": "ダウンロード先: {{path}}",
|
||||||
"refreshSuccess": "更新しました",
|
"refreshSuccess": "更新しました",
|
||||||
"refreshFailed": "更新に失敗しました"
|
"refreshFailed": "更新に失敗しました",
|
||||||
|
"downloadFile": "ファイルをダウンロード",
|
||||||
|
"deleteFile": "ファイルを削除"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,7 @@
|
|||||||
"statusAdded": "新增",
|
"statusAdded": "新增",
|
||||||
"statusDeleted": "删除",
|
"statusDeleted": "删除",
|
||||||
"statusReordered": "已移动",
|
"statusReordered": "已移动",
|
||||||
"restore": "恢复"
|
"restore": "恢复",
|
||||||
|
"fileLimitWarning": "此区块文件数超过30个,请减少文件后再部署。",
|
||||||
|
"deployDisabledReason": "一个或多个区块超过文件数量限制(30)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,7 @@
|
|||||||
"downloadFailed": "下载失败",
|
"downloadFailed": "下载失败",
|
||||||
"downloadAllSuccess": "已下载到: {{path}}",
|
"downloadAllSuccess": "已下载到: {{path}}",
|
||||||
"refreshSuccess": "刷新成功",
|
"refreshSuccess": "刷新成功",
|
||||||
"refreshFailed": "刷新失败"
|
"refreshFailed": "刷新失败",
|
||||||
|
"downloadFile": "下载文件",
|
||||||
|
"deleteFile": "删除文件"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ export type ViewMode = "list" | "code";
|
|||||||
|
|
||||||
export interface SelectedFile {
|
export interface SelectedFile {
|
||||||
type: "js" | "css";
|
type: "js" | "css";
|
||||||
fileKey: string;
|
fileKey?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** For locally added files: absolute path on disk */
|
||||||
|
storagePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionState {
|
interface SessionState {
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ export interface GetFileContentParams {
|
|||||||
fileKey: string;
|
fileKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetLocalFileContentParams {
|
||||||
|
storagePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalFileContent {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
mimeType: string;
|
||||||
|
content: string; // Base64 encoded
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Deploy IPC Types ====================
|
// ==================== Deploy IPC Types ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,6 +208,7 @@ export interface ElectronAPI {
|
|||||||
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
||||||
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
||||||
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
|
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
|
||||||
|
getLocalFileContent: (params: GetLocalFileContentParams) => Promise<Result<LocalFileContent>>;
|
||||||
|
|
||||||
// Deploy
|
// Deploy
|
||||||
deploy: (params: DeployParams) => Promise<Result<DeployResult>>;
|
deploy: (params: DeployParams) => Promise<Result<DeployResult>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user