update reload funtion
This commit is contained in:
68
MEMORY.md
68
MEMORY.md
@@ -1,6 +1,71 @@
|
||||
# MEMORY.md
|
||||
|
||||
## 2026-03-17 - GAIA_BL01 部署错误修复
|
||||
## 2026-03-17 - GAIA_BL01 部署错误修复(更新)
|
||||
|
||||
### 遇到什么问题
|
||||
|
||||
- 用户点击部署时,如果存在未修改的文件(status: "unchanged"),会报错:`[404] [GAIA_BL01] 指定したファイル(id: XXXXX)が見つかりません。`
|
||||
- **之前分析不够准确**:原匹配逻辑只用文件名匹配,如果存在同名文件会匹配到错误的 fileKey
|
||||
|
||||
### 根本原因(更正)
|
||||
|
||||
1. **匹配逻辑缺陷**:只用文件名匹配,同名文件会返回第一个匹配的错误 fileKey
|
||||
2. **类型守卫过于严格**:`isFileResource()` 要求 `fileKey` 存在才返回 true,可能导致有效文件被跳过
|
||||
3. Kintone Customization API 的正确用法:
|
||||
- 从 `Get Customization` 获取的 fileKey 是实时有效的
|
||||
- 只要文件附加到 Customization,fileKey 就永久有效
|
||||
- 参考:https://docs-customine.gusuku.io/en/error/gaia_bl01/
|
||||
|
||||
### 如何解决的
|
||||
|
||||
修改 `src/main/ipc-handlers.ts` 中的匹配逻辑:
|
||||
|
||||
1. **新增 `isFileType()` 类型守卫**:只检查 `type === "FILE"`,不要求 `fileKey` 存在
|
||||
2. **实现三级匹配策略**:
|
||||
- 优先级 1:用前端传来的 `fileKey` 精确匹配(最可靠)
|
||||
- 优先级 2:用 URL 精确匹配(针对 URL 类型)
|
||||
- 优先级 3:用文件名匹配(fallback,同名文件可能出错)
|
||||
3. 添加详细调试日志和验证检查
|
||||
|
||||
```typescript
|
||||
// 新增类型守卫
|
||||
function isFileType(resource): boolean {
|
||||
return resource.type === "FILE" && !!resource.file;
|
||||
}
|
||||
|
||||
// 三级匹配策略
|
||||
if (file.fileKey) {
|
||||
matchingFile = currentFiles?.find(
|
||||
(f) => isFileType(f) && f.file.fileKey === file.fileKey,
|
||||
);
|
||||
}
|
||||
if (!matchingFile && file.url) {
|
||||
matchingFile = currentFiles?.find(
|
||||
(f) => isUrlResource(f) && f.url === file.url,
|
||||
);
|
||||
}
|
||||
if (!matchingFile) {
|
||||
matchingFile = currentFiles?.find(
|
||||
(f) => isFileType(f) && f.file.name === file.fileName,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 以后如何避免
|
||||
|
||||
1. **匹配逻辑优先使用唯一标识符**:不要只用名称匹配,优先使用 ID、key 等唯一标识
|
||||
2. **类型守卫要区分"类型检查"和"有效性验证"**:
|
||||
- 类型检查:`type === "FILE"`
|
||||
- 有效性验证:`fileKey` 是否存在
|
||||
- 这两个应该分开处理
|
||||
3. **Kintone fileKey 的生命周期**:
|
||||
- 用于 Customization:永久有效(只要附加到 App)
|
||||
- 用于记录附件:3 天内必须使用
|
||||
4. **添加调试日志**:在复杂的匹配逻辑中添加调试日志,便于排查问题
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-17 - GAIA_BL01 部署错误修复(初始记录 - 已过时)
|
||||
|
||||
### 遇到什么问题
|
||||
|
||||
@@ -27,7 +92,6 @@
|
||||
|
||||
---
|
||||
|
||||
|
||||
# MEMORY.md
|
||||
|
||||
## 2026-03-15 - CSS 模板字符串语法错误
|
||||
|
||||
@@ -16,7 +16,10 @@ export type MainErrorKey =
|
||||
| "unknownError"
|
||||
| "rollbackNotImplemented"
|
||||
| "encryptPasswordFailed"
|
||||
| "decryptPasswordFailed";
|
||||
| "decryptPasswordFailed"
|
||||
| "deployFailed"
|
||||
| "deployCancelled"
|
||||
| "deployTimeout";
|
||||
|
||||
/**
|
||||
* Error messages by locale
|
||||
@@ -31,6 +34,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
||||
rollbackNotImplemented: "回滚功能尚未实现",
|
||||
encryptPasswordFailed: "密码加密失败",
|
||||
decryptPasswordFailed: "密码解密失败",
|
||||
deployFailed: "部署失败",
|
||||
deployCancelled: "部署已取消",
|
||||
deployTimeout: "部署超时,请稍后刷新查看结果",
|
||||
},
|
||||
"en-US": {
|
||||
domainNotFound: "Domain not found",
|
||||
@@ -41,6 +47,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
||||
rollbackNotImplemented: "Rollback not yet implemented",
|
||||
encryptPasswordFailed: "Failed to encrypt password",
|
||||
decryptPasswordFailed: "Failed to decrypt password",
|
||||
deployFailed: "Deployment failed",
|
||||
deployCancelled: "Deployment cancelled",
|
||||
deployTimeout: "Deployment timed out, please refresh later",
|
||||
},
|
||||
"ja-JP": {
|
||||
domainNotFound: "ドメインが見つかりません",
|
||||
@@ -51,6 +60,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
||||
rollbackNotImplemented: "ロールバック機能はまだ実装されていません",
|
||||
encryptPasswordFailed: "パスワードの暗号化に失敗しました",
|
||||
decryptPasswordFailed: "パスワードの復号化に失敗しました",
|
||||
deployFailed: "デプロイに失敗しました",
|
||||
deployCancelled: "デプロイがキャンセルされました",
|
||||
deployTimeout: "デプロイがタイムアウトしました。後で更新してください",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,13 @@ import type {
|
||||
BackupMetadata,
|
||||
DownloadFile,
|
||||
} from "@shared/types/version";
|
||||
import { isFileResource, isUrlResource, type AppCustomizeParameter, type AppDetail } from "@shared/types/kintone";
|
||||
import {
|
||||
FileConfigResponse,
|
||||
isFileResource,
|
||||
isUrlResource,
|
||||
type AppCustomizeParameter,
|
||||
type AppDetail,
|
||||
} from "@shared/types/kintone";
|
||||
import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
||||
import { getErrorMessage } from "./errors";
|
||||
|
||||
@@ -96,12 +102,49 @@ function handle<P = void, T = unknown>(
|
||||
const data = await handler(params as P);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : getErrorMessage("unknownError");
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: getErrorMessage("unknownError");
|
||||
return { success: false, error: message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Kintone deployment to complete
|
||||
* Polls getDeployStatus until SUCCESS, FAIL, CANCEL, or timeout
|
||||
*/
|
||||
async function waitForDeploySuccess(
|
||||
client: KintoneClient,
|
||||
appId: string,
|
||||
options: { timeoutMs?: number; pollIntervalMs?: number } = {},
|
||||
): Promise<void> {
|
||||
const { timeoutMs = 60000, pollIntervalMs = 1000 } = options;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
const status = await client.getDeployStatus(appId);
|
||||
|
||||
if (status === "SUCCESS") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "FAIL") {
|
||||
throw new Error(getErrorMessage("deployFailed"));
|
||||
}
|
||||
|
||||
if (status === "CANCEL") {
|
||||
throw new Error(getErrorMessage("deployCancelled"));
|
||||
}
|
||||
|
||||
// status === "PROCESSING" - wait and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
throw new Error(getErrorMessage("deployTimeout"));
|
||||
}
|
||||
|
||||
// ==================== Domain Management IPC Handlers ====================
|
||||
|
||||
/**
|
||||
@@ -304,7 +347,7 @@ async function addFilesToBackup(
|
||||
client: KintoneClient,
|
||||
appDetail: AppDetail,
|
||||
backupFiles: Map<string, Buffer>,
|
||||
backupFileList: DownloadFile[]
|
||||
backupFileList: DownloadFile[],
|
||||
): Promise<void> {
|
||||
const files = appDetail.customization?.[platform]?.[fileType] || [];
|
||||
|
||||
@@ -353,10 +396,38 @@ function registerDeploy(): void {
|
||||
const backupFiles = new Map<string, Buffer>();
|
||||
const backupFileList: BackupMetadata["files"] = [];
|
||||
|
||||
await addFilesToBackup("desktop", "js", client, appDetail, backupFiles, backupFileList);
|
||||
await addFilesToBackup("desktop", "css", client, appDetail, backupFiles, backupFileList);
|
||||
await addFilesToBackup("mobile", "js", client, appDetail, backupFiles, backupFileList);
|
||||
await addFilesToBackup("mobile", "css", client, appDetail, backupFiles, backupFileList);
|
||||
await addFilesToBackup(
|
||||
"desktop",
|
||||
"js",
|
||||
client,
|
||||
appDetail,
|
||||
backupFiles,
|
||||
backupFileList,
|
||||
);
|
||||
await addFilesToBackup(
|
||||
"desktop",
|
||||
"css",
|
||||
client,
|
||||
appDetail,
|
||||
backupFiles,
|
||||
backupFileList,
|
||||
);
|
||||
await addFilesToBackup(
|
||||
"mobile",
|
||||
"js",
|
||||
client,
|
||||
appDetail,
|
||||
backupFiles,
|
||||
backupFileList,
|
||||
);
|
||||
await addFilesToBackup(
|
||||
"mobile",
|
||||
"css",
|
||||
client,
|
||||
appDetail,
|
||||
backupFiles,
|
||||
backupFileList,
|
||||
);
|
||||
|
||||
const backupMetadata: BackupMetadata = {
|
||||
backedUpAt: new Date().toISOString(),
|
||||
@@ -381,16 +452,114 @@ function registerDeploy(): void {
|
||||
for (const file of params.files) {
|
||||
if (file.status === "deleted") continue;
|
||||
|
||||
type FileEntry = { type: "FILE"; file: { fileKey: string } } | { type: "URL"; url: string };
|
||||
type FileEntry =
|
||||
| { type: "FILE"; file: { fileKey: string } }
|
||||
| { type: "URL"; url: string };
|
||||
let entry: FileEntry;
|
||||
|
||||
if (file.status === "unchanged") {
|
||||
// IMPORTANT: Get fresh fileKey from current Kintone config, not from frontend
|
||||
// The fileKey from frontend may have been consumed by a previous deployment
|
||||
// and would cause GAIA_BL01 error
|
||||
const currentFiles =
|
||||
file.platform === "desktop"
|
||||
? file.fileType === "js"
|
||||
? appDetail.customization?.desktop?.js
|
||||
: appDetail.customization?.desktop?.css
|
||||
: file.fileType === "js"
|
||||
? appDetail.customization?.mobile?.js
|
||||
: appDetail.customization?.mobile?.css;
|
||||
|
||||
// Strategy for matching unchanged files:
|
||||
// 1. First, try to match by fileKey (most reliable for FILE type)
|
||||
// 2. If not found, fall back to name matching (for URL type or if fileKey changed)
|
||||
//
|
||||
// Note: We use isFileType() instead of isFileResource() for matching
|
||||
// because isFileResource() requires fileKey to exist, which could cause
|
||||
// valid files to be skipped during matching.
|
||||
|
||||
let matchingFile: FileConfigResponse | undefined;
|
||||
let matchMethod: string = "unknown";
|
||||
|
||||
// Step 1: Try exact fileKey match (most reliable)
|
||||
if (file.fileKey) {
|
||||
entry = { type: "FILE", file: { fileKey: file.fileKey } };
|
||||
} else if (file.url) {
|
||||
entry = { type: "URL", url: file.url };
|
||||
matchingFile = currentFiles?.find(
|
||||
(f) => isFileResource(f) && f.file.fileKey === file.fileKey,
|
||||
);
|
||||
if (matchingFile) {
|
||||
matchMethod = "fileKey exact match";
|
||||
console.log(`[DEPLOY DEBUG] Matched by fileKey: ${file.fileKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Try URL match for URL-type files
|
||||
if (!matchingFile && file.url) {
|
||||
matchingFile = currentFiles?.find(
|
||||
(f) => isUrlResource(f) && f.url === file.url,
|
||||
);
|
||||
if (matchingFile) {
|
||||
matchMethod = "URL exact match";
|
||||
console.log(`[DEPLOY DEBUG] Matched by URL: ${file.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Fall back to name matching (less reliable, could match wrong file if duplicates exist)
|
||||
if (!matchingFile) {
|
||||
matchingFile = currentFiles?.find((f) => {
|
||||
if (isFileResource(f)) {
|
||||
return f.file.name === file.fileName;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (matchingFile) {
|
||||
matchMethod = "filename match (fallback)";
|
||||
console.log(
|
||||
`[DEPLOY DEBUG] Matched by filename (fallback): ${file.fileName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DEPLOY DEBUG] Final matching result for "${file.fileName}":`,
|
||||
matchingFile
|
||||
? `${matchMethod} → ${isFileResource(matchingFile) ? `FILE key="${matchingFile.file.fileKey}"` : `URL "${matchingFile.url}"`}`
|
||||
: "NOT FOUND",
|
||||
);
|
||||
|
||||
if (matchingFile) {
|
||||
if (isFileResource(matchingFile)) {
|
||||
// Validate that the matched file has a valid fileKey
|
||||
if (!matchingFile.file.fileKey) {
|
||||
throw new Error(
|
||||
`Matched file "${file.fileName}" has no fileKey in Kintone config. ` +
|
||||
`This indicates corrupted data. Please refresh and try again.`,
|
||||
);
|
||||
}
|
||||
// Verify filename matches (sanity check)
|
||||
if (matchingFile.file.name !== file.fileName) {
|
||||
console.warn(
|
||||
`[DEPLOY WARNING] Filename mismatch: expected "${file.fileName}", found "${matchingFile.file.name}". ` +
|
||||
`Proceeding with matched fileKey.`,
|
||||
);
|
||||
}
|
||||
entry = {
|
||||
type: "FILE",
|
||||
file: { fileKey: matchingFile.file.fileKey },
|
||||
};
|
||||
} else if (isUrlResource(matchingFile)) {
|
||||
entry = { type: "URL", url: matchingFile.url };
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid file type in Kintone config for "${file.fileName}"`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid unchanged file entry: no fileKey or url for "${file.fileName}"`);
|
||||
// File not found in current Kintone config - this is an error
|
||||
// The file may have been deleted from Kintone externally
|
||||
throw new Error(
|
||||
`File "${file.fileName}" not found in current Kintone configuration. ` +
|
||||
`It may have been deleted externally. Please refresh and try again.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// added: read from disk and upload
|
||||
@@ -414,6 +583,9 @@ function registerDeploy(): void {
|
||||
await client.updateAppCustomize(params.appId, newConfig);
|
||||
await client.deployApp(params.appId);
|
||||
|
||||
// Wait for deployment to complete before returning
|
||||
await waitForDeploySuccess(client, params.appId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
backupPath,
|
||||
@@ -690,14 +862,14 @@ function registerCheckForUpdates(): void {
|
||||
}
|
||||
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
|
||||
|
||||
if (result && result.updateInfo) {
|
||||
const currentVersion = app.getVersion();
|
||||
const latestVersion = result.updateInfo.version;
|
||||
|
||||
|
||||
// Compare versions
|
||||
const hasUpdate = latestVersion !== currentVersion;
|
||||
|
||||
|
||||
return {
|
||||
hasUpdate,
|
||||
updateInfo: hasUpdate
|
||||
@@ -709,7 +881,7 @@ function registerCheckForUpdates(): void {
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
hasUpdate: false,
|
||||
updateInfo: undefined,
|
||||
@@ -781,26 +953,32 @@ export function registerIpcHandlers(): void {
|
||||
* Show save dialog
|
||||
*/
|
||||
function registerShowSaveDialog(): void {
|
||||
handle<{ defaultPath?: string }, string | null>("showSaveDialog", async (params) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: params.defaultPath,
|
||||
filters: [
|
||||
{ name: "JavaScript", extensions: ["js"] },
|
||||
{ name: "CSS", extensions: ["css"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
});
|
||||
return result.filePath || null;
|
||||
});
|
||||
handle<{ defaultPath?: string }, string | null>(
|
||||
"showSaveDialog",
|
||||
async (params) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: params.defaultPath,
|
||||
filters: [
|
||||
{ name: "JavaScript", extensions: ["js"] },
|
||||
{ name: "CSS", extensions: ["css"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
});
|
||||
return result.filePath || null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file content to disk
|
||||
*/
|
||||
function registerSaveFileContent(): void {
|
||||
handle<{ filePath: string; content: string }, void>("saveFileContent", async (params) => {
|
||||
const fs = await import("fs");
|
||||
const buffer = Buffer.from(params.content, "base64");
|
||||
await fs.promises.writeFile(params.filePath, buffer);
|
||||
});
|
||||
handle<{ filePath: string; content: string }, void>(
|
||||
"saveFileContent",
|
||||
async (params) => {
|
||||
const fs = await import("fs");
|
||||
const buffer = Buffer.from(params.content, "base64");
|
||||
await fs.promises.writeFile(params.filePath, buffer);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class KintoneClient {
|
||||
return this.withErrorHandling(async () => {
|
||||
const [appInfo, customizeInfo] = await Promise.all([
|
||||
this.client.app.getApp({ id: appId }),
|
||||
this.client.app.getAppCustomize({ app: appId }),
|
||||
this.client.app.getAppCustomize({ app: appId, preview: true }),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,8 +25,7 @@ import {
|
||||
useFileChangeStore,
|
||||
} from "@renderer/stores";
|
||||
import { CodeViewer } from "../CodeViewer";
|
||||
import { isFileResource, isUrlResource } from "@shared/types/kintone";
|
||||
import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
||||
import { transformCustomizeToFiles } from "@shared/utils/fileTransform";
|
||||
import type { DeployFileEntry } from "@shared/types/ipc";
|
||||
import FileSection from "./FileSection";
|
||||
|
||||
@@ -115,71 +114,7 @@ const AppDetail: React.FC = () => {
|
||||
setSelectedFile(null);
|
||||
}, [selectedAppId]);
|
||||
|
||||
// Load app detail when selected
|
||||
useEffect(() => {
|
||||
if (currentDomain && selectedAppId) {
|
||||
loadAppDetail();
|
||||
}
|
||||
}, [currentDomain, selectedAppId]);
|
||||
|
||||
// Initialize file change store from Kintone data
|
||||
useEffect(() => {
|
||||
if (!currentApp || !currentDomain || !selectedAppId) return;
|
||||
|
||||
const customize = currentApp.customization;
|
||||
if (!customize) return;
|
||||
|
||||
const files = [
|
||||
...(customize.desktop?.js ?? []).map((file, index) => ({
|
||||
id: crypto.randomUUID(),
|
||||
fileName: getDisplayName(file, "js", index),
|
||||
fileType: "js" as const,
|
||||
platform: "desktop" as const,
|
||||
size: isFileResource(file)
|
||||
? parseInt(file.file.size ?? "0", 10) || undefined
|
||||
: undefined,
|
||||
fileKey: getFileKey(file),
|
||||
url: isUrlResource(file) ? file.url : undefined,
|
||||
})),
|
||||
...(customize.desktop?.css ?? []).map((file, index) => ({
|
||||
id: crypto.randomUUID(),
|
||||
fileName: getDisplayName(file, "css", index),
|
||||
fileType: "css" as const,
|
||||
platform: "desktop" as const,
|
||||
size: isFileResource(file)
|
||||
? parseInt(file.file.size ?? "0", 10) || undefined
|
||||
: undefined,
|
||||
fileKey: getFileKey(file),
|
||||
url: isUrlResource(file) ? file.url : undefined,
|
||||
})),
|
||||
...(customize.mobile?.js ?? []).map((file, index) => ({
|
||||
id: crypto.randomUUID(),
|
||||
fileName: getDisplayName(file, "js", index),
|
||||
fileType: "js" as const,
|
||||
platform: "mobile" as const,
|
||||
size: isFileResource(file)
|
||||
? parseInt(file.file.size ?? "0", 10) || undefined
|
||||
: undefined,
|
||||
fileKey: getFileKey(file),
|
||||
url: isUrlResource(file) ? file.url : undefined,
|
||||
})),
|
||||
...(customize.mobile?.css ?? []).map((file, index) => ({
|
||||
id: crypto.randomUUID(),
|
||||
fileName: getDisplayName(file, "css", index),
|
||||
fileType: "css" as const,
|
||||
platform: "mobile" as const,
|
||||
size: isFileResource(file)
|
||||
? parseInt(file.file.size ?? "0", 10) || undefined
|
||||
: undefined,
|
||||
fileKey: getFileKey(file),
|
||||
url: isUrlResource(file) ? file.url : undefined,
|
||||
})),
|
||||
];
|
||||
|
||||
fileChangeStore.initializeApp(currentDomain.id, selectedAppId, files);
|
||||
}, [currentApp?.appId]);
|
||||
|
||||
const loadAppDetail = async () => {
|
||||
const loadAppDetail = useCallback(async () => {
|
||||
if (!currentDomain || !selectedAppId) return undefined;
|
||||
|
||||
setLoading(true);
|
||||
@@ -191,6 +126,8 @@ const AppDetail: React.FC = () => {
|
||||
|
||||
// Check if we're still on the same app and component is mounted before updating
|
||||
if (result.success) {
|
||||
// Clear changes and reload from Kintone
|
||||
fileChangeStore.clearChanges(currentDomain.id, selectedAppId);
|
||||
setCurrentApp(result.data);
|
||||
return result.data;
|
||||
}
|
||||
@@ -201,7 +138,25 @@ const AppDetail: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [currentDomain, selectedAppId, setCurrentApp, setLoading]);
|
||||
|
||||
// Load app detail when selected
|
||||
useEffect(() => {
|
||||
if (currentDomain && selectedAppId) {
|
||||
loadAppDetail();
|
||||
}
|
||||
}, [currentDomain, selectedAppId, loadAppDetail]);
|
||||
|
||||
// Initialize file change store from Kintone data
|
||||
useEffect(() => {
|
||||
if (!currentApp || !currentDomain || !selectedAppId) return;
|
||||
|
||||
const customize = currentApp.customization;
|
||||
if (!customize) return;
|
||||
|
||||
const files = transformCustomizeToFiles(customize);
|
||||
fileChangeStore.initializeApp(currentDomain.id, selectedAppId, files);
|
||||
}, [currentApp]);
|
||||
|
||||
const handleFileClick = useCallback((fileKey: string, name: string) => {
|
||||
const ext = name.split(".").pop()?.toLowerCase();
|
||||
@@ -210,6 +165,21 @@ const AppDetail: React.FC = () => {
|
||||
setViewMode("code");
|
||||
}, []);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (!currentDomain || !selectedAppId || refreshing) return;
|
||||
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadAppDetail();
|
||||
message.success(t("refreshSuccess", { ns: "common" }));
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh:", error);
|
||||
message.error(t("refreshFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [currentDomain, selectedAppId, refreshing, fileChangeStore, loadAppDetail, message, t]);
|
||||
|
||||
const handleBackToList = useCallback(() => {
|
||||
setViewMode("list");
|
||||
setSelectedFile(null);
|
||||
@@ -333,8 +303,6 @@ const AppDetail: React.FC = () => {
|
||||
|
||||
if (result.success) {
|
||||
message.success(t("deploySuccess"));
|
||||
// Clear changes and reload from Kintone
|
||||
fileChangeStore.clearChanges(currentDomain.id, selectedAppId);
|
||||
await loadAppDetail();
|
||||
} else {
|
||||
message.error(result.error || t("deployFailed"));
|
||||
@@ -344,7 +312,7 @@ const AppDetail: React.FC = () => {
|
||||
} finally {
|
||||
setDeploying(false);
|
||||
}
|
||||
}, [currentDomain, selectedAppId, deploying, fileChangeStore, message, t]);
|
||||
}, [currentDomain, selectedAppId, deploying, fileChangeStore, loadAppDetail, message, t]);
|
||||
|
||||
if (!currentDomain || !selectedAppId) {
|
||||
return (
|
||||
|
||||
@@ -176,7 +176,6 @@ const FileSection: React.FC<FileSectionProps> = ({
|
||||
};
|
||||
|
||||
addFile(domainId, appId, entry);
|
||||
message.success(t("fileAdded", { name: result.data.fileName }));
|
||||
} catch {
|
||||
message.error(t("fileAddFailed"));
|
||||
} finally {
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"dropFileHere": "Drop file here",
|
||||
"fileTypeNotSupported": "Only {{expected}} files are supported",
|
||||
"fileSizeExceeded": "File size exceeds the 20 MB limit",
|
||||
"fileAdded": "{{name}} added",
|
||||
"fileAddFailed": "Failed to add file",
|
||||
"fileDeleteFailed": "Failed to delete file",
|
||||
"statusAdded": "New",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"dropFileHere": "ここにドロップ",
|
||||
"fileTypeNotSupported": "{{expected}} ファイルのみ対応しています",
|
||||
"fileSizeExceeded": "ファイルサイズが 20 MB 制限を超えています",
|
||||
"fileAdded": "{{name}} を追加しました",
|
||||
"fileAddFailed": "ファイルの追加に失敗しました",
|
||||
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
||||
"statusAdded": "新規",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"dropFileHere": "松开以添加文件",
|
||||
"fileTypeNotSupported": "仅支持 {{expected}} 文件",
|
||||
"fileSizeExceeded": "文件大小超过 20 MB 限制",
|
||||
"fileAdded": "{{name}} 已添加",
|
||||
"fileAddFailed": "添加文件失败",
|
||||
"fileDeleteFailed": "删除文件失败",
|
||||
"statusAdded": "新增",
|
||||
|
||||
@@ -55,6 +55,17 @@ export interface GetFileContentParams {
|
||||
|
||||
// ==================== Deploy IPC Types ====================
|
||||
|
||||
/**
|
||||
* File for deployment - used in UI before actual deployment
|
||||
* This is a renderer-side type for managing files to be deployed
|
||||
*/
|
||||
export interface DeployFile {
|
||||
content: string;
|
||||
fileName: string;
|
||||
fileType: "js" | "css";
|
||||
position: string;
|
||||
}
|
||||
|
||||
export type FileStatus = "unchanged" | "added" | "deleted";
|
||||
|
||||
export interface DeployFileEntry {
|
||||
|
||||
79
src/shared/utils/fileTransform.ts
Normal file
79
src/shared/utils/fileTransform.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type {
|
||||
AppCustomizeResponse,
|
||||
FileConfigResponse,
|
||||
} from "@shared/types/kintone";
|
||||
import { isFileResource, isUrlResource } from "@shared/types/kintone";
|
||||
import type { FileEntry } from "@renderer/stores/fileChangeStore";
|
||||
import { getDisplayName, getFileKey } from "./fileDisplay";
|
||||
|
||||
/**
|
||||
* Transform Kintone customize data into FileEntry array format
|
||||
* Used to initialize file change store from Kintone API response
|
||||
*/
|
||||
export function transformCustomizeToFiles(
|
||||
customize: AppCustomizeResponse | undefined,
|
||||
): Array<Omit<FileEntry, "status">> {
|
||||
if (!customize) return [];
|
||||
|
||||
const files: Array<Omit<FileEntry, "status">> = [];
|
||||
|
||||
// Desktop JS files
|
||||
if (customize.desktop?.js) {
|
||||
files.push(
|
||||
...customize.desktop.js.map((file, index) =>
|
||||
transformFileConfig(file, "js", "desktop", index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop CSS files
|
||||
if (customize.desktop?.css) {
|
||||
files.push(
|
||||
...customize.desktop.css.map((file, index) =>
|
||||
transformFileConfig(file, "css", "desktop", index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile JS files
|
||||
if (customize.mobile?.js) {
|
||||
files.push(
|
||||
...customize.mobile.js.map((file, index) =>
|
||||
transformFileConfig(file, "js", "mobile", index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile CSS files
|
||||
if (customize.mobile?.css) {
|
||||
files.push(
|
||||
...customize.mobile.css.map((file, index) =>
|
||||
transformFileConfig(file, "css", "mobile", index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single file config into FileEntry format
|
||||
*/
|
||||
function transformFileConfig(
|
||||
file: FileConfigResponse,
|
||||
fileType: "js" | "css",
|
||||
platform: "desktop" | "mobile",
|
||||
index: number,
|
||||
): Omit<FileEntry, "status"> {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
fileName: getDisplayName(file, fileType, index),
|
||||
fileType,
|
||||
platform,
|
||||
size: isFileResource(file)
|
||||
? parseInt(file.file.size ?? "0", 10) || undefined
|
||||
: undefined,
|
||||
fileKey: getFileKey(file),
|
||||
url: isUrlResource(file) ? file.url : undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user