update reload funtion
This commit is contained in:
68
MEMORY.md
68
MEMORY.md
@@ -1,6 +1,71 @@
|
|||||||
# MEMORY.md
|
# 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
|
# MEMORY.md
|
||||||
|
|
||||||
## 2026-03-15 - CSS 模板字符串语法错误
|
## 2026-03-15 - CSS 模板字符串语法错误
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export type MainErrorKey =
|
|||||||
| "unknownError"
|
| "unknownError"
|
||||||
| "rollbackNotImplemented"
|
| "rollbackNotImplemented"
|
||||||
| "encryptPasswordFailed"
|
| "encryptPasswordFailed"
|
||||||
| "decryptPasswordFailed";
|
| "decryptPasswordFailed"
|
||||||
|
| "deployFailed"
|
||||||
|
| "deployCancelled"
|
||||||
|
| "deployTimeout";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error messages by locale
|
* Error messages by locale
|
||||||
@@ -31,6 +34,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
|||||||
rollbackNotImplemented: "回滚功能尚未实现",
|
rollbackNotImplemented: "回滚功能尚未实现",
|
||||||
encryptPasswordFailed: "密码加密失败",
|
encryptPasswordFailed: "密码加密失败",
|
||||||
decryptPasswordFailed: "密码解密失败",
|
decryptPasswordFailed: "密码解密失败",
|
||||||
|
deployFailed: "部署失败",
|
||||||
|
deployCancelled: "部署已取消",
|
||||||
|
deployTimeout: "部署超时,请稍后刷新查看结果",
|
||||||
},
|
},
|
||||||
"en-US": {
|
"en-US": {
|
||||||
domainNotFound: "Domain not found",
|
domainNotFound: "Domain not found",
|
||||||
@@ -41,6 +47,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
|||||||
rollbackNotImplemented: "Rollback not yet implemented",
|
rollbackNotImplemented: "Rollback not yet implemented",
|
||||||
encryptPasswordFailed: "Failed to encrypt password",
|
encryptPasswordFailed: "Failed to encrypt password",
|
||||||
decryptPasswordFailed: "Failed to decrypt password",
|
decryptPasswordFailed: "Failed to decrypt password",
|
||||||
|
deployFailed: "Deployment failed",
|
||||||
|
deployCancelled: "Deployment cancelled",
|
||||||
|
deployTimeout: "Deployment timed out, please refresh later",
|
||||||
},
|
},
|
||||||
"ja-JP": {
|
"ja-JP": {
|
||||||
domainNotFound: "ドメインが見つかりません",
|
domainNotFound: "ドメインが見つかりません",
|
||||||
@@ -51,6 +60,9 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
|
|||||||
rollbackNotImplemented: "ロールバック機能はまだ実装されていません",
|
rollbackNotImplemented: "ロールバック機能はまだ実装されていません",
|
||||||
encryptPasswordFailed: "パスワードの暗号化に失敗しました",
|
encryptPasswordFailed: "パスワードの暗号化に失敗しました",
|
||||||
decryptPasswordFailed: "パスワードの復号化に失敗しました",
|
decryptPasswordFailed: "パスワードの復号化に失敗しました",
|
||||||
|
deployFailed: "デプロイに失敗しました",
|
||||||
|
deployCancelled: "デプロイがキャンセルされました",
|
||||||
|
deployTimeout: "デプロイがタイムアウトしました。後で更新してください",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,13 @@ import type {
|
|||||||
BackupMetadata,
|
BackupMetadata,
|
||||||
DownloadFile,
|
DownloadFile,
|
||||||
} from "@shared/types/version";
|
} 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 { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
||||||
import { getErrorMessage } from "./errors";
|
import { getErrorMessage } from "./errors";
|
||||||
|
|
||||||
@@ -96,12 +102,49 @@ function handle<P = void, T = unknown>(
|
|||||||
const data = await handler(params as P);
|
const data = await handler(params as P);
|
||||||
return { success: true, data };
|
return { success: true, data };
|
||||||
} catch (error) {
|
} 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 };
|
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 ====================
|
// ==================== Domain Management IPC Handlers ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,7 +347,7 @@ async function addFilesToBackup(
|
|||||||
client: KintoneClient,
|
client: KintoneClient,
|
||||||
appDetail: AppDetail,
|
appDetail: AppDetail,
|
||||||
backupFiles: Map<string, Buffer>,
|
backupFiles: Map<string, Buffer>,
|
||||||
backupFileList: DownloadFile[]
|
backupFileList: DownloadFile[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const files = appDetail.customization?.[platform]?.[fileType] || [];
|
const files = appDetail.customization?.[platform]?.[fileType] || [];
|
||||||
|
|
||||||
@@ -353,10 +396,38 @@ function registerDeploy(): void {
|
|||||||
const backupFiles = new Map<string, Buffer>();
|
const backupFiles = new Map<string, Buffer>();
|
||||||
const backupFileList: BackupMetadata["files"] = [];
|
const backupFileList: BackupMetadata["files"] = [];
|
||||||
|
|
||||||
await addFilesToBackup("desktop", "js", client, appDetail, backupFiles, backupFileList);
|
await addFilesToBackup(
|
||||||
await addFilesToBackup("desktop", "css", client, appDetail, backupFiles, backupFileList);
|
"desktop",
|
||||||
await addFilesToBackup("mobile", "js", client, appDetail, backupFiles, backupFileList);
|
"js",
|
||||||
await addFilesToBackup("mobile", "css", client, appDetail, backupFiles, backupFileList);
|
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 = {
|
const backupMetadata: BackupMetadata = {
|
||||||
backedUpAt: new Date().toISOString(),
|
backedUpAt: new Date().toISOString(),
|
||||||
@@ -381,16 +452,114 @@ function registerDeploy(): void {
|
|||||||
for (const file of params.files) {
|
for (const file of params.files) {
|
||||||
if (file.status === "deleted") continue;
|
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;
|
let entry: FileEntry;
|
||||||
|
|
||||||
if (file.status === "unchanged") {
|
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) {
|
if (file.fileKey) {
|
||||||
entry = { type: "FILE", file: { fileKey: file.fileKey } };
|
matchingFile = currentFiles?.find(
|
||||||
} else if (file.url) {
|
(f) => isFileResource(f) && f.file.fileKey === file.fileKey,
|
||||||
entry = { type: "URL", url: file.url };
|
);
|
||||||
|
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 {
|
} 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 {
|
} else {
|
||||||
// added: read from disk and upload
|
// added: read from disk and upload
|
||||||
@@ -414,6 +583,9 @@ function registerDeploy(): void {
|
|||||||
await client.updateAppCustomize(params.appId, newConfig);
|
await client.updateAppCustomize(params.appId, newConfig);
|
||||||
await client.deployApp(params.appId);
|
await client.deployApp(params.appId);
|
||||||
|
|
||||||
|
// Wait for deployment to complete before returning
|
||||||
|
await waitForDeploySuccess(client, params.appId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
backupPath,
|
backupPath,
|
||||||
@@ -781,26 +953,32 @@ export function registerIpcHandlers(): void {
|
|||||||
* Show save dialog
|
* Show save dialog
|
||||||
*/
|
*/
|
||||||
function registerShowSaveDialog(): void {
|
function registerShowSaveDialog(): void {
|
||||||
handle<{ defaultPath?: string }, string | null>("showSaveDialog", async (params) => {
|
handle<{ defaultPath?: string }, string | null>(
|
||||||
const result = await dialog.showSaveDialog({
|
"showSaveDialog",
|
||||||
defaultPath: params.defaultPath,
|
async (params) => {
|
||||||
filters: [
|
const result = await dialog.showSaveDialog({
|
||||||
{ name: "JavaScript", extensions: ["js"] },
|
defaultPath: params.defaultPath,
|
||||||
{ name: "CSS", extensions: ["css"] },
|
filters: [
|
||||||
{ name: "All Files", extensions: ["*"] },
|
{ name: "JavaScript", extensions: ["js"] },
|
||||||
],
|
{ name: "CSS", extensions: ["css"] },
|
||||||
});
|
{ name: "All Files", extensions: ["*"] },
|
||||||
return result.filePath || null;
|
],
|
||||||
});
|
});
|
||||||
|
return result.filePath || null;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save file content to disk
|
* Save file content to disk
|
||||||
*/
|
*/
|
||||||
function registerSaveFileContent(): void {
|
function registerSaveFileContent(): void {
|
||||||
handle<{ filePath: string; content: string }, void>("saveFileContent", async (params) => {
|
handle<{ filePath: string; content: string }, void>(
|
||||||
const fs = await import("fs");
|
"saveFileContent",
|
||||||
const buffer = Buffer.from(params.content, "base64");
|
async (params) => {
|
||||||
await fs.promises.writeFile(params.filePath, buffer);
|
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 () => {
|
return this.withErrorHandling(async () => {
|
||||||
const [appInfo, customizeInfo] = await Promise.all([
|
const [appInfo, customizeInfo] = await Promise.all([
|
||||||
this.client.app.getApp({ id: appId }),
|
this.client.app.getApp({ id: appId }),
|
||||||
this.client.app.getAppCustomize({ app: appId }),
|
this.client.app.getAppCustomize({ app: appId, preview: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ import {
|
|||||||
useFileChangeStore,
|
useFileChangeStore,
|
||||||
} from "@renderer/stores";
|
} from "@renderer/stores";
|
||||||
import { CodeViewer } from "../CodeViewer";
|
import { CodeViewer } from "../CodeViewer";
|
||||||
import { isFileResource, isUrlResource } from "@shared/types/kintone";
|
import { transformCustomizeToFiles } from "@shared/utils/fileTransform";
|
||||||
import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
|
||||||
import type { DeployFileEntry } from "@shared/types/ipc";
|
import type { DeployFileEntry } from "@shared/types/ipc";
|
||||||
import FileSection from "./FileSection";
|
import FileSection from "./FileSection";
|
||||||
|
|
||||||
@@ -115,71 +114,7 @@ const AppDetail: React.FC = () => {
|
|||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
}, [selectedAppId]);
|
}, [selectedAppId]);
|
||||||
|
|
||||||
// Load app detail when selected
|
const loadAppDetail = useCallback(async () => {
|
||||||
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 () => {
|
|
||||||
if (!currentDomain || !selectedAppId) return undefined;
|
if (!currentDomain || !selectedAppId) return undefined;
|
||||||
|
|
||||||
setLoading(true);
|
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
|
// Check if we're still on the same app and component is mounted before updating
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
// Clear changes and reload from Kintone
|
||||||
|
fileChangeStore.clearChanges(currentDomain.id, selectedAppId);
|
||||||
setCurrentApp(result.data);
|
setCurrentApp(result.data);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
@@ -201,7 +138,25 @@ const AppDetail: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 handleFileClick = useCallback((fileKey: string, name: string) => {
|
||||||
const ext = name.split(".").pop()?.toLowerCase();
|
const ext = name.split(".").pop()?.toLowerCase();
|
||||||
@@ -210,6 +165,21 @@ const AppDetail: React.FC = () => {
|
|||||||
setViewMode("code");
|
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(() => {
|
const handleBackToList = useCallback(() => {
|
||||||
setViewMode("list");
|
setViewMode("list");
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
@@ -333,8 +303,6 @@ const AppDetail: React.FC = () => {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.success(t("deploySuccess"));
|
message.success(t("deploySuccess"));
|
||||||
// Clear changes and reload from Kintone
|
|
||||||
fileChangeStore.clearChanges(currentDomain.id, selectedAppId);
|
|
||||||
await loadAppDetail();
|
await loadAppDetail();
|
||||||
} else {
|
} else {
|
||||||
message.error(result.error || t("deployFailed"));
|
message.error(result.error || t("deployFailed"));
|
||||||
@@ -344,7 +312,7 @@ const AppDetail: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setDeploying(false);
|
setDeploying(false);
|
||||||
}
|
}
|
||||||
}, [currentDomain, selectedAppId, deploying, fileChangeStore, message, t]);
|
}, [currentDomain, selectedAppId, deploying, fileChangeStore, loadAppDetail, message, t]);
|
||||||
|
|
||||||
if (!currentDomain || !selectedAppId) {
|
if (!currentDomain || !selectedAppId) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ const FileSection: React.FC<FileSectionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
addFile(domainId, appId, entry);
|
addFile(domainId, appId, entry);
|
||||||
message.success(t("fileAdded", { name: result.data.fileName }));
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error(t("fileAddFailed"));
|
message.error(t("fileAddFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"dropFileHere": "Drop file here",
|
"dropFileHere": "Drop file here",
|
||||||
"fileTypeNotSupported": "Only {{expected}} files are supported",
|
"fileTypeNotSupported": "Only {{expected}} files are supported",
|
||||||
"fileSizeExceeded": "File size exceeds the 20 MB limit",
|
"fileSizeExceeded": "File size exceeds the 20 MB limit",
|
||||||
"fileAdded": "{{name}} added",
|
|
||||||
"fileAddFailed": "Failed to add file",
|
"fileAddFailed": "Failed to add file",
|
||||||
"fileDeleteFailed": "Failed to delete file",
|
"fileDeleteFailed": "Failed to delete file",
|
||||||
"statusAdded": "New",
|
"statusAdded": "New",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"dropFileHere": "ここにドロップ",
|
"dropFileHere": "ここにドロップ",
|
||||||
"fileTypeNotSupported": "{{expected}} ファイルのみ対応しています",
|
"fileTypeNotSupported": "{{expected}} ファイルのみ対応しています",
|
||||||
"fileSizeExceeded": "ファイルサイズが 20 MB 制限を超えています",
|
"fileSizeExceeded": "ファイルサイズが 20 MB 制限を超えています",
|
||||||
"fileAdded": "{{name}} を追加しました",
|
|
||||||
"fileAddFailed": "ファイルの追加に失敗しました",
|
"fileAddFailed": "ファイルの追加に失敗しました",
|
||||||
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
"fileDeleteFailed": "ファイルの削除に失敗しました",
|
||||||
"statusAdded": "新規",
|
"statusAdded": "新規",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"dropFileHere": "松开以添加文件",
|
"dropFileHere": "松开以添加文件",
|
||||||
"fileTypeNotSupported": "仅支持 {{expected}} 文件",
|
"fileTypeNotSupported": "仅支持 {{expected}} 文件",
|
||||||
"fileSizeExceeded": "文件大小超过 20 MB 限制",
|
"fileSizeExceeded": "文件大小超过 20 MB 限制",
|
||||||
"fileAdded": "{{name}} 已添加",
|
|
||||||
"fileAddFailed": "添加文件失败",
|
"fileAddFailed": "添加文件失败",
|
||||||
"fileDeleteFailed": "删除文件失败",
|
"fileDeleteFailed": "删除文件失败",
|
||||||
"statusAdded": "新增",
|
"statusAdded": "新增",
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ export interface GetFileContentParams {
|
|||||||
|
|
||||||
// ==================== Deploy IPC Types ====================
|
// ==================== 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 type FileStatus = "unchanged" | "added" | "deleted";
|
||||||
|
|
||||||
export interface DeployFileEntry {
|
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