chore: adjust printWidth to 160 and format code

This commit is contained in:
2026-03-18 00:24:52 +08:00
parent 3de55b83f0
commit 47d3bd0124
42 changed files with 414 additions and 1247 deletions

View File

@@ -1,7 +1,7 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"trailingComma": "es5", "trailingComma": "es5",
"printWidth": 200, "printWidth": 160,
"semi": true, "semi": true,
"singleQuote": false "singleQuote": false
} }

6
src/main/env.d.ts vendored
View File

@@ -1,10 +1,10 @@
/// <reference types="electron-vite/node" /> /// <reference types="electron-vite/node" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly MAIN_VITE_API_URL: string readonly MAIN_VITE_API_URL: string;
readonly MAIN_VITE_DEBUG: string readonly MAIN_VITE_DEBUG: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv;
} }

View File

@@ -27,8 +27,7 @@ export type MainErrorKey =
const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = { const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
"zh-CN": { "zh-CN": {
domainNotFound: "域名未找到", domainNotFound: "域名未找到",
domainDuplicate: domainDuplicate: '域名 "{{domain}}" 与用户 "{{username}}" 已存在。请编辑现有域名。',
'域名 "{{domain}}" 与用户 "{{username}}" 已存在。请编辑现有域名。',
connectionFailed: "连接失败", connectionFailed: "连接失败",
unknownError: "未知错误", unknownError: "未知错误",
rollbackNotImplemented: "回滚功能尚未实现", rollbackNotImplemented: "回滚功能尚未实现",
@@ -40,8 +39,7 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
}, },
"en-US": { "en-US": {
domainNotFound: "Domain not found", domainNotFound: "Domain not found",
domainDuplicate: domainDuplicate: 'Domain "{{domain}}" with user "{{username}}" already exists. Please edit the existing domain instead.',
'Domain "{{domain}}" with user "{{username}}" already exists. Please edit the existing domain instead.',
connectionFailed: "Connection failed", connectionFailed: "Connection failed",
unknownError: "Unknown error", unknownError: "Unknown error",
rollbackNotImplemented: "Rollback not yet implemented", rollbackNotImplemented: "Rollback not yet implemented",
@@ -53,8 +51,7 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
}, },
"ja-JP": { "ja-JP": {
domainNotFound: "ドメインが見つかりません", domainNotFound: "ドメインが見つかりません",
domainDuplicate: domainDuplicate: 'ドメイン "{{domain}}" とユーザー "{{username}}" は既に存在します。既存のドメインを編集してください。',
'ドメイン "{{domain}}" とユーザー "{{username}}" は既に存在します。既存のドメインを編集してください。',
connectionFailed: "接続に失敗しました", connectionFailed: "接続に失敗しました",
unknownError: "不明なエラー", unknownError: "不明なエラー",
rollbackNotImplemented: "ロールバック機能はまだ実装されていません", rollbackNotImplemented: "ロールバック機能はまだ実装されていません",
@@ -101,11 +98,7 @@ function interpolate(message: string, params?: ErrorParams): string {
* @param params - Optional interpolation parameters * @param params - Optional interpolation parameters
* @param locale - Optional locale override (defaults to stored preference) * @param locale - Optional locale override (defaults to stored preference)
*/ */
export function getErrorMessage( export function getErrorMessage(key: MainErrorKey, params?: ErrorParams, locale?: LocaleCode): string {
key: MainErrorKey,
params?: ErrorParams,
locale?: LocaleCode,
): string {
const targetLocale = locale ?? getLocale(); const targetLocale = locale ?? getLocale();
const messages = errorMessages[targetLocale] || errorMessages["zh-CN"]; const messages = errorMessages[targetLocale] || errorMessages["zh-CN"];
const message = messages[key] || errorMessages["zh-CN"][key] || key; const message = messages[key] || errorMessages["zh-CN"][key] || key;

View File

@@ -2,11 +2,7 @@ import { app, shell, BrowserWindow, ipcMain } from "electron";
import { join } from "path"; import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { registerIpcHandlers } from "./ipc-handlers"; import { registerIpcHandlers } from "./ipc-handlers";
import { import { initializeStorage, isSecureStorageAvailable, getStorageBackend } from "./storage";
initializeStorage,
isSecureStorageAvailable,
getStorageBackend,
} from "./storage";
function createWindow(): void { function createWindow(): void {
// Create the browser window. // Create the browser window.
@@ -71,9 +67,7 @@ app.whenReady().then(() => {
// Check secure storage availability // Check secure storage availability
if (!isSecureStorageAvailable()) { if (!isSecureStorageAvailable()) {
console.warn( console.warn(`Warning: Secure storage not available (backend: ${getStorageBackend()})`);
`Warning: Secure storage not available (backend: ${getStorageBackend()})`,
);
console.warn("Passwords will not be securely encrypted on this system."); console.warn("Passwords will not be securely encrypted on this system.");
} }

View File

@@ -46,24 +46,9 @@ import type {
FileDeleteParams, FileDeleteParams,
} from "@shared/types/ipc"; } from "@shared/types/ipc";
import type { LocaleCode } from "@shared/types/locale"; import type { LocaleCode } from "@shared/types/locale";
import type { import type { Domain, DomainWithStatus, DomainWithPassword } from "@shared/types/domain";
Domain, import type { Version, DownloadMetadata, BackupMetadata, DownloadFile } from "@shared/types/version";
DomainWithStatus, import { FileConfigResponse, isFileResource, isUrlResource, type AppCustomizeParameter, type AppDetail } from "@shared/types/kintone";
DomainWithPassword,
} from "@shared/types/domain";
import type {
Version,
DownloadMetadata,
BackupMetadata,
DownloadFile,
} from "@shared/types/version";
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";
@@ -91,10 +76,7 @@ async function getClient(domainId: string): Promise<KintoneClient> {
/** /**
* Helper to wrap IPC handlers with error handling * Helper to wrap IPC handlers with error handling
*/ */
function handle<P = void, T = unknown>( function handle<P = void, T = unknown>(channel: string, handler: (params: P) => Promise<T>): void {
channel: string,
handler: (params: P) => Promise<T>,
): void {
ipcMain.handle(channel, async (_event, params: P): Promise<Result<T>> => { ipcMain.handle(channel, async (_event, params: P): Promise<Result<T>> => {
try { try {
// For handlers without params (P=void), params will be undefined but handler ignores it // For handlers without params (P=void), params will be undefined but handler ignores it
@@ -102,10 +84,7 @@ 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 = const message = error instanceof Error ? error.message : getErrorMessage("unknownError");
error instanceof Error
? error.message
: getErrorMessage("unknownError");
return { success: false, error: message }; return { success: false, error: message };
} }
}); });
@@ -115,11 +94,7 @@ function handle<P = void, T = unknown>(
* Wait for Kintone deployment to complete * Wait for Kintone deployment to complete
* Polls getDeployStatus until SUCCESS, FAIL, CANCEL, or timeout * Polls getDeployStatus until SUCCESS, FAIL, CANCEL, or timeout
*/ */
async function waitForDeploySuccess( async function waitForDeploySuccess(client: KintoneClient, appId: string, options: { timeoutMs?: number; pollIntervalMs?: number } = {}): Promise<void> {
client: KintoneClient,
appId: string,
options: { timeoutMs?: number; pollIntervalMs?: number } = {},
): Promise<void> {
const { timeoutMs = 60000, pollIntervalMs = 1000 } = options; const { timeoutMs = 60000, pollIntervalMs = 1000 } = options;
const startTime = Date.now(); const startTime = Date.now();
@@ -165,9 +140,7 @@ function registerCreateDomain(): void {
// Check for duplicate domain+username // Check for duplicate domain+username
const existingDomains = await listDomains(); const existingDomains = await listDomains();
const duplicate = existingDomains.find( const duplicate = existingDomains.find(
(d) => (d) => d.domain.toLowerCase() === params.domain.toLowerCase() && d.username.toLowerCase() === params.username.toLowerCase()
d.domain.toLowerCase() === params.domain.toLowerCase() &&
d.username.toLowerCase() === params.username.toLowerCase(),
); );
if (duplicate) { if (duplicate) {
@@ -175,7 +148,7 @@ function registerCreateDomain(): void {
getErrorMessage("domainDuplicate", { getErrorMessage("domainDuplicate", {
domain: params.domain, domain: params.domain,
username: params.username, username: params.username,
}), })
); );
} }
@@ -267,29 +240,26 @@ function registerTestConnection(): void {
* Test domain connection with temporary credentials * Test domain connection with temporary credentials
*/ */
function registerTestDomainConnection(): void { function registerTestDomainConnection(): void {
handle<TestDomainConnectionParams, boolean>( handle<TestDomainConnectionParams, boolean>("testDomainConnection", async (params) => {
"testDomainConnection", const tempDomain: DomainWithPassword = {
async (params) => { id: "temp",
const tempDomain: DomainWithPassword = { name: "temp",
id: "temp", domain: params.domain,
name: "temp", username: params.username,
domain: params.domain, password: params.password || "",
username: params.username, createdAt: new Date().toISOString(),
password: params.password || "", updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(), };
updatedAt: new Date().toISOString(),
};
const client = createKintoneClient(tempDomain); const client = createKintoneClient(tempDomain);
const result = await client.testConnection(); const result = await client.testConnection();
if (!result.success) { if (!result.success) {
throw new Error(result.error || getErrorMessage("connectionFailed")); throw new Error(result.error || getErrorMessage("connectionFailed"));
} }
return true; return true;
}, });
);
} }
// ==================== Browse IPC Handlers ==================== // ==================== Browse IPC Handlers ====================
@@ -298,26 +268,20 @@ function registerTestDomainConnection(): void {
* Get apps * Get apps
*/ */
function registerGetApps(): void { function registerGetApps(): void {
handle<GetAppsParams, Awaited<ReturnType<KintoneClient["getApps"]>>>( handle<GetAppsParams, Awaited<ReturnType<KintoneClient["getApps"]>>>("getApps", async (params) => {
"getApps", const client = await getClient(params.domainId);
async (params) => { return client.getApps({
const client = await getClient(params.domainId); limit: params.limit,
return client.getApps({ offset: params.offset,
limit: params.limit, });
offset: params.offset, });
});
},
);
} }
/** /**
* Get app detail * Get app detail
*/ */
function registerGetAppDetail(): void { function registerGetAppDetail(): void {
handle< handle<GetAppDetailParams, Awaited<ReturnType<KintoneClient["getAppDetail"]>>>("getAppDetail", async (params) => {
GetAppDetailParams,
Awaited<ReturnType<KintoneClient["getAppDetail"]>>
>("getAppDetail", async (params) => {
const client = await getClient(params.domainId); const client = await getClient(params.domainId);
return client.getAppDetail(params.appId); return client.getAppDetail(params.appId);
}); });
@@ -327,10 +291,7 @@ function registerGetAppDetail(): void {
* Get file content * Get file content
*/ */
function registerGetFileContent(): void { function registerGetFileContent(): void {
handle< handle<GetFileContentParams, Awaited<ReturnType<KintoneClient["getFileContent"]>>>("getFileContent", async (params) => {
GetFileContentParams,
Awaited<ReturnType<KintoneClient["getFileContent"]>>
>("getFileContent", async (params) => {
const client = await getClient(params.domainId); const client = await getClient(params.domainId);
return client.getFileContent(params.fileKey); return client.getFileContent(params.fileKey);
}); });
@@ -347,7 +308,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] || [];
@@ -396,38 +357,10 @@ 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( await addFilesToBackup("desktop", "js", client, appDetail, backupFiles, backupFileList);
"desktop", await addFilesToBackup("desktop", "css", client, appDetail, backupFiles, backupFileList);
"js", await addFilesToBackup("mobile", "js", client, appDetail, backupFiles, backupFileList);
client, await addFilesToBackup("mobile", "css", client, appDetail, backupFiles, backupFileList);
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(),
@@ -452,9 +385,7 @@ 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 FileEntry = { type: "FILE"; file: { fileKey: string } } | { type: "URL"; url: string };
| { type: "FILE"; file: { fileKey: string } }
| { type: "URL"; url: string };
let entry: FileEntry; let entry: FileEntry;
if (file.status === "unchanged") { if (file.status === "unchanged") {
@@ -483,9 +414,7 @@ function registerDeploy(): void {
// Step 1: Try exact fileKey match (most reliable) // Step 1: Try exact fileKey match (most reliable)
if (file.fileKey) { if (file.fileKey) {
matchingFile = currentFiles?.find( matchingFile = currentFiles?.find((f) => isFileResource(f) && f.file.fileKey === file.fileKey);
(f) => isFileResource(f) && f.file.fileKey === file.fileKey,
);
if (matchingFile) { if (matchingFile) {
matchMethod = "fileKey exact match"; matchMethod = "fileKey exact match";
console.log(`[DEPLOY DEBUG] Matched by fileKey: ${file.fileKey}`); console.log(`[DEPLOY DEBUG] Matched by fileKey: ${file.fileKey}`);
@@ -494,9 +423,7 @@ function registerDeploy(): void {
// Step 2: Try URL match for URL-type files // Step 2: Try URL match for URL-type files
if (!matchingFile && file.url) { if (!matchingFile && file.url) {
matchingFile = currentFiles?.find( matchingFile = currentFiles?.find((f) => isUrlResource(f) && f.url === file.url);
(f) => isUrlResource(f) && f.url === file.url,
);
if (matchingFile) { if (matchingFile) {
matchMethod = "URL exact match"; matchMethod = "URL exact match";
console.log(`[DEPLOY DEBUG] Matched by URL: ${file.url}`); console.log(`[DEPLOY DEBUG] Matched by URL: ${file.url}`);
@@ -513,9 +440,7 @@ function registerDeploy(): void {
}); });
if (matchingFile) { if (matchingFile) {
matchMethod = "filename match (fallback)"; matchMethod = "filename match (fallback)";
console.log( console.log(`[DEPLOY DEBUG] Matched by filename (fallback): ${file.fileName}`);
`[DEPLOY DEBUG] Matched by filename (fallback): ${file.fileName}`,
);
} }
} }
@@ -523,7 +448,7 @@ function registerDeploy(): void {
`[DEPLOY DEBUG] Final matching result for "${file.fileName}":`, `[DEPLOY DEBUG] Final matching result for "${file.fileName}":`,
matchingFile matchingFile
? `${matchMethod}${isFileResource(matchingFile) ? `FILE key="${matchingFile.file.fileKey}"` : `URL "${matchingFile.url}"`}` ? `${matchMethod}${isFileResource(matchingFile) ? `FILE key="${matchingFile.file.fileKey}"` : `URL "${matchingFile.url}"`}`
: "NOT FOUND", : "NOT FOUND"
); );
if (matchingFile) { if (matchingFile) {
@@ -531,15 +456,13 @@ function registerDeploy(): void {
// Validate that the matched file has a valid fileKey // Validate that the matched file has a valid fileKey
if (!matchingFile.file.fileKey) { if (!matchingFile.file.fileKey) {
throw new Error( throw new Error(
`Matched file "${file.fileName}" has no fileKey in Kintone config. ` + `Matched file "${file.fileName}" has no fileKey in Kintone config. ` + `This indicates corrupted data. Please refresh and try again.`
`This indicates corrupted data. Please refresh and try again.`,
); );
} }
// Verify filename matches (sanity check) // Verify filename matches (sanity check)
if (matchingFile.file.name !== file.fileName) { if (matchingFile.file.name !== file.fileName) {
console.warn( console.warn(
`[DEPLOY WARNING] Filename mismatch: expected "${file.fileName}", found "${matchingFile.file.name}". ` + `[DEPLOY WARNING] Filename mismatch: expected "${file.fileName}", found "${matchingFile.file.name}". ` + `Proceeding with matched fileKey.`
`Proceeding with matched fileKey.`,
); );
} }
entry = { entry = {
@@ -549,16 +472,13 @@ function registerDeploy(): void {
} else if (isUrlResource(matchingFile)) { } else if (isUrlResource(matchingFile)) {
entry = { type: "URL", url: matchingFile.url }; entry = { type: "URL", url: matchingFile.url };
} else { } else {
throw new Error( throw new Error(`Invalid file type in Kintone config for "${file.fileName}"`);
`Invalid file type in Kintone config for "${file.fileName}"`,
);
} }
} else { } else {
// File not found in current Kintone config - this is an error // File not found in current Kintone config - this is an error
// The file may have been deleted from Kintone externally // The file may have been deleted from Kintone externally
throw new Error( throw new Error(
`File "${file.fileName}" not found in current Kintone configuration. ` + `File "${file.fileName}" not found in current Kintone configuration. ` + `It may have been deleted externally. Please refresh and try again.`
`It may have been deleted externally. Please refresh and try again.`,
); );
} }
} else { } else {
@@ -633,12 +553,7 @@ function registerDownload(): void {
const downloadFileList: DownloadMetadata["files"] = []; const downloadFileList: DownloadMetadata["files"] = [];
// Download based on requested file types // Download based on requested file types
const fileTypes = params.fileTypes || [ const fileTypes = params.fileTypes || ["pc_js", "pc_css", "mobile_js", "mobile_css"];
"pc_js",
"pc_css",
"mobile_js",
"mobile_css",
];
for (const fileType of fileTypes) { for (const fileType of fileTypes) {
const files = const files =
@@ -699,88 +614,82 @@ function registerDownload(): void {
* Download all files as ZIP * Download all files as ZIP
*/ */
function registerDownloadAllZip(): void { function registerDownloadAllZip(): void {
handle<DownloadAllZipParams, DownloadAllZipResult>( handle<DownloadAllZipParams, DownloadAllZipResult>("downloadAllZip", async (params) => {
"downloadAllZip", const client = await getClient(params.domainId);
async (params) => { const domainWithPassword = await getDomain(params.domainId);
const client = await getClient(params.domainId);
const domainWithPassword = await getDomain(params.domainId);
if (!domainWithPassword) { if (!domainWithPassword) {
throw new Error(getErrorMessage("domainNotFound")); throw new Error(getErrorMessage("domainNotFound"));
}
const appDetail = await client.getAppDetail(params.appId);
const zip = new AdmZip();
const metadata = {
downloadedAt: new Date().toISOString(),
domain: domainWithPassword.domain,
appId: params.appId,
appName: appDetail.name,
};
// Download and add PC files
const pcJsFiles = appDetail.customization?.desktop?.js || [];
const pcCssFiles = appDetail.customization?.desktop?.css || [];
for (const [index, file] of pcJsFiles.entries()) {
const fileKey = getFileKey(file);
if (fileKey) {
const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64");
const fileName = getDisplayName(file, "js", index);
zip.addFile(`desktop-js/${fileName}`, buffer);
} }
}
const appDetail = await client.getAppDetail(params.appId); for (const [index, file] of pcCssFiles.entries()) {
const zip = new AdmZip(); const fileKey = getFileKey(file);
const metadata = { if (fileKey) {
downloadedAt: new Date().toISOString(), const content = await client.getFileContent(fileKey);
domain: domainWithPassword.domain, const buffer = Buffer.from(content.content || "", "base64");
appId: params.appId, const fileName = getDisplayName(file, "css", index);
appName: appDetail.name, zip.addFile(`desktop-css/${fileName}`, buffer);
};
// Download and add PC files
const pcJsFiles = appDetail.customization?.desktop?.js || [];
const pcCssFiles = appDetail.customization?.desktop?.css || [];
for (const [index, file] of pcJsFiles.entries()) {
const fileKey = getFileKey(file);
if (fileKey) {
const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64");
const fileName = getDisplayName(file, "js", index);
zip.addFile(`desktop-js/${fileName}`, buffer);
}
} }
}
for (const [index, file] of pcCssFiles.entries()) { // Download and add Mobile files
const fileKey = getFileKey(file); const mobileJsFiles = appDetail.customization?.mobile?.js || [];
if (fileKey) { const mobileCssFiles = appDetail.customization?.mobile?.css || [];
const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64"); for (const [index, file] of mobileJsFiles.entries()) {
const fileName = getDisplayName(file, "css", index); const fileKey = getFileKey(file);
zip.addFile(`desktop-css/${fileName}`, buffer); if (fileKey) {
} const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64");
const fileName = getDisplayName(file, "js", index);
zip.addFile(`mobile-js/${fileName}`, buffer);
} }
}
// Download and add Mobile files for (const [index, file] of mobileCssFiles.entries()) {
const mobileJsFiles = appDetail.customization?.mobile?.js || []; const fileKey = getFileKey(file);
const mobileCssFiles = appDetail.customization?.mobile?.css || []; if (fileKey) {
const content = await client.getFileContent(fileKey);
for (const [index, file] of mobileJsFiles.entries()) { const buffer = Buffer.from(content.content || "", "base64");
const fileKey = getFileKey(file); const fileName = getDisplayName(file, "css", index);
if (fileKey) { zip.addFile(`mobile-css/${fileName}`, buffer);
const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64");
const fileName = getDisplayName(file, "js", index);
zip.addFile(`mobile-js/${fileName}`, buffer);
}
} }
}
for (const [index, file] of mobileCssFiles.entries()) { // Add metadata.json
const fileKey = getFileKey(file); zip.addFile("metadata.json", Buffer.from(JSON.stringify(metadata, null, 2), "utf-8"));
if (fileKey) {
const content = await client.getFileContent(fileKey);
const buffer = Buffer.from(content.content || "", "base64");
const fileName = getDisplayName(file, "css", index);
zip.addFile(`mobile-css/${fileName}`, buffer);
}
}
// Add metadata.json // Write ZIP to user-provided path
zip.addFile( zip.writeZip(params.savePath);
"metadata.json",
Buffer.from(JSON.stringify(metadata, null, 2), "utf-8"),
);
// Write ZIP to user-provided path return {
zip.writeZip(params.savePath); success: true,
path: params.savePath,
return { };
success: true, });
path: params.savePath,
};
},
);
} }
// ==================== Version IPC Handlers ==================== // ==================== Version IPC Handlers ====================
@@ -953,32 +862,26 @@ export function registerIpcHandlers(): void {
* Show save dialog * Show save dialog
*/ */
function registerShowSaveDialog(): void { function registerShowSaveDialog(): void {
handle<{ defaultPath?: string }, string | null>( handle<{ defaultPath?: string }, string | null>("showSaveDialog", async (params) => {
"showSaveDialog", const result = await dialog.showSaveDialog({
async (params) => { defaultPath: params.defaultPath,
const result = await dialog.showSaveDialog({ filters: [
defaultPath: params.defaultPath, { name: "JavaScript", extensions: ["js"] },
filters: [ { name: "CSS", extensions: ["css"] },
{ name: "JavaScript", extensions: ["js"] }, { name: "All Files", extensions: ["*"] },
{ 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>( handle<{ filePath: string; content: string }, void>("saveFileContent", async (params) => {
"saveFileContent", const fs = await import("fs");
async (params) => { const buffer = Buffer.from(params.content, "base64");
const fs = await import("fs"); await fs.promises.writeFile(params.filePath, buffer);
const buffer = Buffer.from(params.content, "base64"); });
await fs.promises.writeFile(params.filePath, buffer);
},
);
} }

View File

@@ -1,13 +1,7 @@
import { KintoneRestAPIClient } from "@kintone/rest-api-client"; import { KintoneRestAPIClient } from "@kintone/rest-api-client";
import type { KintoneRestAPIError } from "@kintone/rest-api-client"; import type { KintoneRestAPIError } from "@kintone/rest-api-client";
import type { DomainWithPassword } from "@shared/types/domain"; import type { DomainWithPassword } from "@shared/types/domain";
import { import { type AppResponse, type AppDetail, type FileContent, type KintoneApiError, AppCustomizeParameter } from "@shared/types/kintone";
type AppResponse,
type AppDetail,
type FileContent,
type KintoneApiError,
AppCustomizeParameter,
} from "@shared/types/kintone";
import { getErrorMessage } from "./errors"; import { getErrorMessage } from "./errors";
/** /**
@@ -18,11 +12,7 @@ export class KintoneError extends Error {
public readonly id?: string; public readonly id?: string;
public readonly statusCode?: number; public readonly statusCode?: number;
constructor( constructor(message: string, apiError?: KintoneApiError, statusCode?: number) {
message: string,
apiError?: KintoneApiError,
statusCode?: number,
) {
super(message); super(message);
this.name = "KintoneError"; this.name = "KintoneError";
this.code = apiError?.code; this.code = apiError?.code;
@@ -53,11 +43,7 @@ export class KintoneClient {
private convertError(error: unknown): KintoneError { private convertError(error: unknown): KintoneError {
if (error && typeof error === "object" && "code" in error) { if (error && typeof error === "object" && "code" in error) {
const apiError = error as KintoneRestAPIError; const apiError = error as KintoneRestAPIError;
return new KintoneError( return new KintoneError(apiError.message, { code: apiError.code, message: apiError.message, id: apiError.id }, apiError.status);
apiError.message,
{ code: apiError.code, message: apiError.message, id: apiError.id },
apiError.status,
);
} }
if (error instanceof Error) { if (error instanceof Error) {
@@ -81,10 +67,7 @@ export class KintoneClient {
* Get all apps with pagination support * Get all apps with pagination support
* Fetches all apps by making multiple requests if needed * Fetches all apps by making multiple requests if needed
*/ */
async getApps(options?: { async getApps(options?: { limit?: number; offset?: number }): Promise<AppResponse[]> {
limit?: number;
offset?: number;
}): Promise<AppResponse[]> {
return this.withErrorHandling(async () => { return this.withErrorHandling(async () => {
// If pagination options provided, use them directly // If pagination options provided, use them directly
if (options?.limit !== undefined || options?.offset !== undefined) { if (options?.limit !== undefined || options?.offset !== undefined) {
@@ -149,11 +132,7 @@ export class KintoneClient {
}); });
} }
async uploadFile( async uploadFile(content: string | Buffer, fileName: string, _mimeType?: string): Promise<{ fileKey: string }> {
content: string | Buffer,
fileName: string,
_mimeType?: string,
): Promise<{ fileKey: string }> {
return this.withErrorHandling(async () => { return this.withErrorHandling(async () => {
const response = await this.client.file.uploadFile({ const response = await this.client.file.uploadFile({
file: { name: fileName, data: content }, file: { name: fileName, data: content },
@@ -164,10 +143,7 @@ export class KintoneClient {
// ==================== Deploy APIs ==================== // ==================== Deploy APIs ====================
async updateAppCustomize( async updateAppCustomize(appId: string, config: Omit<AppCustomizeParameter, "app">): Promise<void> {
appId: string,
config: Omit<AppCustomizeParameter, "app">,
): Promise<void> {
return this.withErrorHandling(async () => { return this.withErrorHandling(async () => {
await this.client.app.updateAppCustomize({ ...config, app: appId }); await this.client.app.updateAppCustomize({ ...config, app: appId });
}); });
@@ -179,9 +155,7 @@ export class KintoneClient {
}); });
} }
async getDeployStatus( async getDeployStatus(appId: string): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
appId: string,
): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
return this.withErrorHandling(async () => { return this.withErrorHandling(async () => {
const response = await this.client.app.getDeployStatus({ apps: [appId] }); const response = await this.client.app.getDeployStatus({ apps: [appId] });
return response.apps[0]?.status || "FAIL"; return response.apps[0]?.status || "FAIL";
@@ -198,10 +172,7 @@ export class KintoneClient {
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
error: error: error instanceof KintoneError ? error.message : getErrorMessage("connectionFailed"),
error instanceof KintoneError
? error.message
: getErrorMessage("connectionFailed"),
}; };
} }
} }

View File

@@ -8,11 +8,7 @@ import { app, safeStorage } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import type { Domain, DomainWithPassword } from "@shared/types/domain"; import type { Domain, DomainWithPassword } from "@shared/types/domain";
import type { import type { Version, DownloadMetadata, BackupMetadata } from "@shared/types/version";
Version,
DownloadMetadata,
BackupMetadata,
} from "@shared/types/version";
import type { LocaleCode } from "@shared/types/locale"; import type { LocaleCode } from "@shared/types/locale";
import { DEFAULT_LOCALE } from "@shared/types/locale"; import { DEFAULT_LOCALE } from "@shared/types/locale";
@@ -107,14 +103,14 @@ function writeJsonFile<T>(filePath: string, data: T): void {
export function isSecureStorageAvailable(): boolean { export function isSecureStorageAvailable(): boolean {
try { try {
// Check if the method exists (added in Electron 30+) // Check if the method exists (added in Electron 30+)
if (typeof safeStorage.getSelectedStorageBackend === 'function') { if (typeof safeStorage.getSelectedStorageBackend === "function") {
const backend = safeStorage.getSelectedStorageBackend() const backend = safeStorage.getSelectedStorageBackend();
return backend !== 'basic_text' return backend !== "basic_text";
} }
// Fallback: check if encryption is available // Fallback: check if encryption is available
return safeStorage.isEncryptionAvailable() return safeStorage.isEncryptionAvailable();
} catch { } catch {
return false return false;
} }
} }
@@ -123,12 +119,12 @@ export function isSecureStorageAvailable(): boolean {
*/ */
export function getStorageBackend(): string { export function getStorageBackend(): string {
try { try {
if (typeof safeStorage.getSelectedStorageBackend === 'function') { if (typeof safeStorage.getSelectedStorageBackend === "function") {
return safeStorage.getSelectedStorageBackend() return safeStorage.getSelectedStorageBackend();
} }
return 'unknown' return "unknown";
} catch { } catch {
return 'unknown' return "unknown";
} }
} }
@@ -139,9 +135,7 @@ export function encryptPassword(password: string): Buffer {
try { try {
return safeStorage.encryptString(password); return safeStorage.encryptString(password);
} catch (error) { } catch (error) {
throw new Error( throw new Error(`Failed to encrypt password: ${error instanceof Error ? error.message : "Unknown error"}`);
`Failed to encrypt password: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} }
} }
@@ -152,9 +146,7 @@ export function decryptPassword(encrypted: Buffer): string {
try { try {
return safeStorage.decryptString(encrypted); return safeStorage.decryptString(encrypted);
} catch (error) { } catch (error) {
throw new Error( throw new Error(`Failed to decrypt password: ${error instanceof Error ? error.message : "Unknown error"}`);
`Failed to decrypt password: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} }
} }
@@ -163,10 +155,7 @@ export function decryptPassword(encrypted: Buffer): string {
/** /**
* Save a domain with encrypted password * Save a domain with encrypted password
*/ */
export async function saveDomain( export async function saveDomain(domain: Domain, password: string): Promise<void> {
domain: Domain,
password: string,
): Promise<void> {
const configPath = getConfigPath(); const configPath = getConfigPath();
const config = readJsonFile<AppConfig>(configPath, { domains: [] }); const config = readJsonFile<AppConfig>(configPath, { domains: [] });
const existingIndex = config.domains.findIndex((d) => d.id === domain.id); const existingIndex = config.domains.findIndex((d) => d.id === domain.id);
@@ -190,9 +179,7 @@ export async function saveDomain(
/** /**
* Get a domain by ID with decrypted password * Get a domain by ID with decrypted password
*/ */
export async function getDomain( export async function getDomain(id: string): Promise<DomainWithPassword | null> {
id: string,
): Promise<DomainWithPassword | null> {
const configPath = getConfigPath(); const configPath = getConfigPath();
const config = readJsonFile<AppConfig>(configPath, { domains: [] }); const config = readJsonFile<AppConfig>(configPath, { domains: [] });
const domain = config.domains.find((d) => d.id === id); const domain = config.domains.find((d) => d.id === id);
@@ -253,13 +240,7 @@ export async function deleteDomain(id: string): Promise<void> {
* Save a version to local storage * Save a version to local storage
*/ */
export async function saveVersion(version: Version): Promise<void> { export async function saveVersion(version: Version): Promise<void> {
const versionDir = getStoragePath( const versionDir = getStoragePath("versions", version.domainId, version.appId, version.fileType, version.id);
"versions",
version.domainId,
version.appId,
version.fileType,
version.id,
);
ensureDir(versionDir); ensureDir(versionDir);
@@ -271,10 +252,7 @@ export async function saveVersion(version: Version): Promise<void> {
/** /**
* List versions for a specific app * List versions for a specific app
*/ */
export async function listVersions( export async function listVersions(domainId: string, appId: string): Promise<Version[]> {
domainId: string,
appId: string,
): Promise<Version[]> {
const versions: Version[] = []; const versions: Version[] = [];
const baseDir = getStoragePath("versions", domainId, appId); const baseDir = getStoragePath("versions", domainId, appId);
@@ -304,9 +282,7 @@ export async function listVersions(
} }
// Sort by createdAt descending // Sort by createdAt descending
return versions.sort( return versions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
} }
/** /**
@@ -349,17 +325,9 @@ export async function deleteVersion(id: string): Promise<void> {
/** /**
* Save downloaded files with metadata * Save downloaded files with metadata
*/ */
export async function saveDownload( export async function saveDownload(metadata: DownloadMetadata, files: Map<string, Buffer>): Promise<string> {
metadata: DownloadMetadata,
files: Map<string, Buffer>,
): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const downloadDir = getStoragePath( const downloadDir = getStoragePath("downloads", metadata.domainId, metadata.appId, timestamp);
"downloads",
metadata.domainId,
metadata.appId,
timestamp,
);
ensureDir(downloadDir); ensureDir(downloadDir);
@@ -392,18 +360,9 @@ export function getDownloadPath(domainId: string, appId?: string): string {
/** /**
* Save backup files with metadata * Save backup files with metadata
*/ */
export async function saveBackup( export async function saveBackup(metadata: BackupMetadata, files: Map<string, Buffer>): Promise<string> {
metadata: BackupMetadata,
files: Map<string, Buffer>,
): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupDir = getStoragePath( const backupDir = getStoragePath("versions", metadata.domainId, metadata.appId, "backup", timestamp);
"versions",
metadata.domainId,
metadata.appId,
"backup",
timestamp,
);
ensureDir(backupDir); ensureDir(backupDir);
@@ -437,12 +396,7 @@ export async function saveCustomizationFile(params: {
}): Promise<{ storagePath: string; fileName: string; size: number }> { }): Promise<{ storagePath: string; fileName: string; size: number }> {
const { domainId, appId, platform, fileType, fileId, sourcePath } = params; const { domainId, appId, platform, fileType, fileId, sourcePath } = params;
const fileName = path.basename(sourcePath); const fileName = path.basename(sourcePath);
const dir = getStoragePath( const dir = getStoragePath("files", domainId, appId, `${platform}_${fileType}`);
"files",
domainId,
appId,
`${platform}_${fileType}`,
);
ensureDir(dir); ensureDir(dir);
const storagePath = path.join(dir, `${fileId}_${fileName}`); const storagePath = path.join(dir, `${fileId}_${fileName}`);
fs.copyFileSync(sourcePath, storagePath); fs.copyFileSync(sourcePath, storagePath);
@@ -453,9 +407,7 @@ export async function saveCustomizationFile(params: {
/** /**
* Delete a customization file from storage. * Delete a customization file from storage.
*/ */
export async function deleteCustomizationFile( export async function deleteCustomizationFile(storagePath: string): Promise<void> {
storagePath: string,
): Promise<void> {
if (fs.existsSync(storagePath)) { if (fs.existsSync(storagePath)) {
fs.unlinkSync(storagePath); fs.unlinkSync(storagePath);
} }

View File

@@ -23,12 +23,7 @@ import type {
FileDeleteParams, FileDeleteParams,
} from "@shared/types/ipc"; } from "@shared/types/ipc";
import type { Domain, DomainWithStatus } from "@shared/types/domain"; import type { Domain, DomainWithStatus } from "@shared/types/domain";
import type { import type { AppResponse, AppDetail, FileContent, KintoneSpace } from "@shared/types/kintone";
AppResponse,
AppDetail,
FileContent,
KintoneSpace,
} from "@shared/types/kintone";
import type { Version } from "@shared/types/version"; import type { Version } from "@shared/types/version";
import type { LocaleCode } from "@shared/types/locale"; import type { LocaleCode } from "@shared/types/locale";
@@ -49,16 +44,12 @@ export interface SelfAPI {
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>; updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
deleteDomain: (id: string) => Promise<Result<void>>; deleteDomain: (id: string) => Promise<Result<void>>;
testConnection: (id: string) => Promise<Result<DomainWithStatus>>; testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
testDomainConnection: ( testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
params: TestDomainConnectionParams,
) => Promise<Result<boolean>>;
// ==================== Browse ==================== // ==================== Browse ====================
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: ( getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
params: GetFileContentParams,
) => Promise<Result<FileContent>>;
// ==================== Deploy ==================== // ==================== Deploy ====================
deploy: (params: DeployParams) => Promise<Result<DeployResult>>; deploy: (params: DeployParams) => Promise<Result<DeployResult>>;

View File

@@ -9,13 +9,7 @@ import { Layout, Typography, Space, Modal } from "antd";
import { Button, Tooltip } from "@lobehub/ui"; import { Button, Tooltip } from "@lobehub/ui";
import { import { Cloud, History, PanelLeftClose, PanelLeftOpen, Settings as SettingsIcon } from "lucide-react";
Cloud,
History,
PanelLeftClose,
PanelLeftOpen,
Settings as SettingsIcon,
} from "lucide-react";
import { createStyles, useTheme } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import { useDomainStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
@@ -158,20 +152,11 @@ const App: React.FC = () => {
}, []); }, []);
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { const { sidebarWidth, siderCollapsed, domainExpanded, setSidebarWidth, setSiderCollapsed, setDomainExpanded } = useUIStore();
sidebarWidth,
siderCollapsed,
domainExpanded,
setSidebarWidth,
setSiderCollapsed,
setDomainExpanded,
} = useUIStore();
const [settingsOpen, setSettingsOpen] = React.useState(false); const [settingsOpen, setSettingsOpen] = React.useState(false);
const [isResizing, setIsResizing] = React.useState(false); const [isResizing, setIsResizing] = React.useState(false);
const domainSectionHeight = domainExpanded const domainSectionHeight = domainExpanded ? DOMAIN_SECTION_EXPANDED : DOMAIN_SECTION_COLLAPSED;
? DOMAIN_SECTION_EXPANDED
: DOMAIN_SECTION_COLLAPSED;
// Handle resize start // Handle resize start
const handleResizeStart = React.useCallback( const handleResizeStart = React.useCallback(
@@ -184,10 +169,7 @@ const App: React.FC = () => {
const handleMouseMove = (moveEvent: MouseEvent) => { const handleMouseMove = (moveEvent: MouseEvent) => {
const delta = moveEvent.clientX - startX; const delta = moveEvent.clientX - startX;
const newWidth = Math.min( const newWidth = Math.min(MAX_SIDER_WIDTH, Math.max(MIN_SIDER_WIDTH, startWidth + delta));
MAX_SIDER_WIDTH,
Math.max(MIN_SIDER_WIDTH, startWidth + delta),
);
setSidebarWidth(newWidth); setSidebarWidth(newWidth);
}; };
@@ -200,7 +182,7 @@ const App: React.FC = () => {
document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
}, },
[sidebarWidth, setSidebarWidth], [sidebarWidth, setSidebarWidth]
); );
const toggleSider = () => { const toggleSider = () => {
@@ -223,29 +205,14 @@ const App: React.FC = () => {
<span className={styles.logoText}>Kintone JS/CSS Manager</span> <span className={styles.logoText}>Kintone JS/CSS Manager</span>
</div> </div>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}> <Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
<Button <Button type="text" icon={<PanelLeftClose size={16} />} onClick={toggleSider} className={styles.siderCloseButton} size="small" />
type="text"
icon={<PanelLeftClose size={16} />}
onClick={toggleSider}
className={styles.siderCloseButton}
size="small"
/>
</Tooltip> </Tooltip>
</div> </div>
<div className={styles.siderContent}> <div className={styles.siderContent}>
<div <div className={styles.domainSection} style={{ height: domainSectionHeight }}>
className={styles.domainSection} <DomainManager collapsed={!domainExpanded} onToggleCollapse={() => setDomainExpanded(!domainExpanded)} />
style={{ height: domainSectionHeight }}
>
<DomainManager
collapsed={!domainExpanded}
onToggleCollapse={() => setDomainExpanded(!domainExpanded)}
/>
</div> </div>
<div <div className={styles.appSection} style={{ height: `calc(100% - ${domainSectionHeight}px)` }}>
className={styles.appSection}
style={{ height: `calc(100% - ${domainSectionHeight}px)` }}
>
<AppList /> <AppList />
</div> </div>
</div> </div>
@@ -262,20 +229,12 @@ const App: React.FC = () => {
</Sider> </Sider>
{/* Main Content */} {/* Main Content */}
<Layout <Layout className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`} style={{ marginLeft: siderCollapsed ? 0 : sidebarWidth }}>
className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`}
style={{ marginLeft: siderCollapsed ? 0 : sidebarWidth }}
>
<Header className={styles.header}> <Header className={styles.header}>
<div className={styles.headerLeft}> <div className={styles.headerLeft}>
{siderCollapsed && ( {siderCollapsed && (
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}> <Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
<Button <Button type="text" icon={<PanelLeftOpen size={16} />} onClick={toggleSider} size="small" />
type="text"
icon={<PanelLeftOpen size={16} />}
onClick={toggleSider}
size="small"
/>
</Tooltip> </Tooltip>
)} )}
<Title level={5} style={{ margin: 0 }}> <Title level={5} style={{ margin: 0 }}>
@@ -287,10 +246,7 @@ const App: React.FC = () => {
{t("versionHistory")} {t("versionHistory")}
</Button> </Button>
<Tooltip title={t("settings")}> <Tooltip title={t("settings")}>
<Button <Button icon={<SettingsIcon size={16} />} onClick={() => setSettingsOpen(true)} />
icon={<SettingsIcon size={16} />}
onClick={() => setSettingsOpen(true)}
/>
</Tooltip> </Tooltip>
</Space> </Space>
</Header> </Header>
@@ -305,14 +261,7 @@ const App: React.FC = () => {
</Layout> </Layout>
{/* Settings Modal */} {/* Settings Modal */}
<Modal <Modal title={t("settings")} open={settingsOpen} onCancel={() => setSettingsOpen(false)} footer={null} width={480} mask={{ closable: false }}>
title={t("settings")}
open={settingsOpen}
onCancel={() => setSettingsOpen(false)}
footer={null}
width={480}
mask={{ closable: false }}
>
<Settings /> <Settings />
</Modal> </Modal>
</Layout> </Layout>

View File

@@ -7,23 +7,9 @@ import React, { useCallback, useEffect } 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 } from "antd";
import { Button, Empty } from "@lobehub/ui"; import { Button, Empty } from "@lobehub/ui";
import { import { LayoutGrid, Download, History, Rocket, Monitor, Smartphone, ArrowLeft, RefreshCw } from "lucide-react";
LayoutGrid,
Download,
History,
Rocket,
Monitor,
Smartphone,
ArrowLeft,
RefreshCw,
} from "lucide-react";
import { createStyles, useTheme } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import { import { useAppStore, useDomainStore, useSessionStore, useFileChangeStore } from "@renderer/stores";
useAppStore,
useDomainStore,
useSessionStore,
useFileChangeStore,
} from "@renderer/stores";
import { CodeViewer } from "../CodeViewer"; import { CodeViewer } from "../CodeViewer";
import { transformCustomizeToFiles } from "@shared/utils/fileTransform"; import { transformCustomizeToFiles } from "@shared/utils/fileTransform";
import type { DeployFileEntry } from "@shared/types/ipc"; import type { DeployFileEntry } from "@shared/types/ipc";
@@ -94,16 +80,12 @@ const AppDetail: React.FC = () => {
const { styles } = useStyles(); const { styles } = useStyles();
const token = useTheme(); const token = useTheme();
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } = const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } = useAppStore();
useAppStore(); const { viewMode, selectedFile, setViewMode, setSelectedFile } = useSessionStore();
const { viewMode, selectedFile, setViewMode, setSelectedFile } =
useSessionStore();
const fileChangeStore = useFileChangeStore(); const fileChangeStore = useFileChangeStore();
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [downloadingKey, setDownloadingKey] = React.useState<string | null>( const [downloadingKey, setDownloadingKey] = React.useState<string | null>(null);
null,
);
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);
@@ -210,11 +192,7 @@ const AppDetail: React.FC = () => {
}); });
if (!contentResult.success || !contentResult.data.content) { if (!contentResult.success || !contentResult.data.content) {
message.error( message.error(contentResult.success ? t("downloadFailed", { ns: "common" }) : contentResult.error);
contentResult.success
? t("downloadFailed", { ns: "common" })
: contentResult.error,
);
return; return;
} }
@@ -226,9 +204,7 @@ const AppDetail: React.FC = () => {
if (saveResult.success) { if (saveResult.success) {
message.success(t("downloadSuccess", { ns: "common" })); message.success(t("downloadSuccess", { ns: "common" }));
} else { } else {
message.error( message.error(saveResult.error || t("downloadFailed", { ns: "common" }));
saveResult.error || t("downloadFailed", { ns: "common" }),
);
} }
} catch { } catch {
message.error(t("downloadFailed", { ns: "common" })); message.error(t("downloadFailed", { ns: "common" }));
@@ -236,12 +212,11 @@ const AppDetail: React.FC = () => {
setDownloadingKey(null); setDownloadingKey(null);
} }
}, },
[currentDomain, downloadingKey, message, t], [currentDomain, downloadingKey, message, t]
); );
const handleDownloadAll = useCallback(async () => { const handleDownloadAll = useCallback(async () => {
if (!currentDomain || !selectedAppId || downloadingAll || !currentApp) if (!currentDomain || !selectedAppId || downloadingAll || !currentApp) return;
return;
const appName = currentApp.name || "app"; const appName = currentApp.name || "app";
const sanitizedAppName = appName.replace(/[:*?"<>|]/g, "_"); const sanitizedAppName = appName.replace(/[:*?"<>|]/g, "_");
@@ -265,9 +240,7 @@ const AppDetail: React.FC = () => {
}); });
if (result.success) { if (result.success) {
message.success( message.success(t("downloadAllSuccess", { path: result.data?.path, ns: "common" }));
t("downloadAllSuccess", { path: result.data?.path, ns: "common" }),
);
} else { } else {
message.error(result.error || t("downloadFailed", { ns: "common" })); message.error(result.error || t("downloadFailed", { ns: "common" }));
} }
@@ -354,30 +327,14 @@ const AppDetail: React.FC = () => {
<Tag>ID: {currentApp.appId}</Tag> <Tag>ID: {currentApp.appId}</Tag>
</div> </div>
<Space> <Space>
<Button <Button icon={<RefreshCw size={16} />} loading={refreshing} onClick={handleRefresh}>
icon={<RefreshCw size={16} />}
loading={refreshing}
onClick={handleRefresh}
>
{t("refresh", { ns: "common" })} {t("refresh", { ns: "common" })}
</Button> </Button>
<Button icon={<History size={16} />}> <Button icon={<History size={16} />}>{t("versionHistory", { ns: "common" })}</Button>
{t("versionHistory", { ns: "common" })} <Button icon={<Download size={16} />} loading={downloadingAll} onClick={handleDownloadAll}>
</Button>
<Button
icon={<Download size={16} />}
loading={downloadingAll}
onClick={handleDownloadAll}
>
{t("downloadAll")} {t("downloadAll")}
</Button> </Button>
<Button <Button type="primary" icon={<Rocket size={16} />} loading={deploying} disabled={!hasChanges && !deploying} onClick={handleDeploy}>
type="primary"
icon={<Rocket size={16} />}
loading={deploying}
disabled={!hasChanges && !deploying}
onClick={handleDeploy}
>
{t("deploy")} {t("deploy")}
{hasChanges && ( {hasChanges && (
<Tag <Tag
@@ -451,21 +408,10 @@ const AppDetail: React.FC = () => {
</> </>
) : ( ) : (
<div className={styles.codeView}> <div className={styles.codeView}>
<Button <Button type="text" icon={<ArrowLeft size={16} />} onClick={handleBackToList} className={styles.backButton}>
type="text"
icon={<ArrowLeft size={16} />}
onClick={handleBackToList}
className={styles.backButton}
>
{t("backToList")} {t("backToList")}
</Button> </Button>
{selectedFile && ( {selectedFile && <CodeViewer fileKey={selectedFile.fileKey} fileName={selectedFile.name} fileType={selectedFile.type} />}
<CodeViewer
fileKey={selectedFile.fileKey}
fileName={selectedFile.name}
fileType={selectedFile.type}
/>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -62,32 +62,16 @@ const DropZone: React.FC<DropZoneProps> = ({ fileType, isSaving, onFileSelected
} }
e.target.value = ""; e.target.value = "";
}, },
[onFileSelected], [onFileSelected]
); );
return ( return (
<> <>
<button <button className={styles.button} onClick={handleClick} disabled={isSaving} type="button">
className={styles.button}
onClick={handleClick}
disabled={isSaving}
type="button"
>
<CloudUpload size={14} /> <CloudUpload size={14} />
<span> <span>{isSaving ? t("loading", { ns: "common" }) : t("dropZoneHint", { fileType: `.${fileType}` })}</span>
{isSaving
? t("loading", { ns: "common" })
: t("dropZoneHint", { fileType: `.${fileType}` })}
</span>
</button> </button>
<input <input ref={inputRef} type="file" accept={`.${fileType}`} multiple style={{ display: "none" }} onChange={handleChange} />
ref={inputRef}
type="file"
accept={`.${fileType}`}
multiple
style={{ display: "none" }}
onChange={handleChange}
/>
</> </>
); );
}; };

View File

@@ -72,14 +72,7 @@ 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> = ({ const FileItem: React.FC<FileItemProps> = ({ entry, onDelete, onRestore, onView, onDownload, isDownloading }) => {
entry,
onDelete,
onRestore,
onView,
onDownload,
isDownloading,
}) => {
const { t } = useTranslation(["app", "common"]); const { t } = useTranslation(["app", "common"]);
const { styles, cx } = useStyles(); const { styles, cx } = useStyles();
const token = useTheme(); const token = useTheme();
@@ -95,96 +88,38 @@ const FileItem: React.FC<FileItemProps> = ({
<div className={styles.item}> <div className={styles.item}>
<div className={styles.fileInfo}> <div className={styles.fileInfo}>
<SortableList.DragHandle /> <SortableList.DragHandle />
{entry.status !== "unchanged" && ( {entry.status !== "unchanged" && <div className={styles.statusDot} style={{ background: statusColor[entry.status] }} />}
<div <MaterialFileTypeIcon type="file" filename={`file.${entry.fileType}`} size={16} />
className={styles.statusDot} <span className={cx(styles.fileName, entry.status === "deleted" && styles.fileNameDeleted)}>{entry.fileName}</span>
style={{ background: statusColor[entry.status] }} {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}
/>
<span
className={cx(
styles.fileName,
entry.status === "deleted" && styles.fileNameDeleted,
)}
>
{entry.fileName}
</span>
{entry.status === "added" && (
<Badge
color={token.colorSuccess}
text={t("statusAdded")}
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
/>
)}
{entry.status === "deleted" && ( {entry.status === "deleted" && (
<Badge <Badge color={token.colorError} text={t("statusDeleted")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
color={token.colorError}
text={t("statusDeleted")}
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
/>
)} )}
{entry.status === "reordered" && ( {entry.status === "reordered" && (
<Badge <Badge color={token.colorWarning} text={t("statusReordered")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
color={token.colorWarning}
text={t("statusReordered")}
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
/>
)} )}
</div> </div>
<Space> <Space>
{entry.size && ( {entry.size && <span className={styles.fileSize}>{formatFileSize(entry.size)}</span>}
<span className={styles.fileSize}>{formatFileSize(entry.size)}</span>
)}
{entry.status === "deleted" ? ( {entry.status === "deleted" ? (
<Button <Button type="text" size="small" icon={<Undo2 size={16} />} onClick={onRestore}>
type="text"
size="small"
icon={<Undo2 size={16} />}
onClick={onRestore}
>
{t("restore")} {t("restore")}
</Button> </Button>
) : ( ) : (
<> <>
{(entry.status === "unchanged" || entry.status === "reordered") && {(entry.status === "unchanged" || entry.status === "reordered") && onView && entry.fileKey && (
onView && <Button type="text" size="small" icon={<Code size={16} />} onClick={onView}>
entry.fileKey && ( {t("view")}
<Button </Button>
type="text" )}
size="small" {(entry.status === "unchanged" || entry.status === "reordered") && onDownload && entry.fileKey && (
icon={<Code size={16} />} <Button type="text" size="small" icon={<Download size={16} />} loading={isDownloading} onClick={onDownload}>
onClick={onView} {t("download", { ns: "common" })}
> </Button>
{t("view")} )}
</Button> <Button type="text" size="small" danger icon={<Trash2 size={16} />} onClick={onDelete} />
)}
{(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> </Space>

View File

@@ -50,7 +50,9 @@ const useStyles = createStyles(({ token, css }) => ({
border: 2px dashed transparent; border: 2px dashed transparent;
border-radius: ${token.borderRadiusLG}px; border-radius: ${token.borderRadiusLG}px;
overflow: hidden; overflow: hidden;
transition: border-color 0.15s, background 0.15s; transition:
border-color 0.15s,
background 0.15s;
`, `,
fileTableBorder: css` fileTableBorder: css`
border: 1px solid ${token.colorBorderSecondary}; border: 1px solid ${token.colorBorderSecondary};
@@ -88,17 +90,7 @@ const useStyles = createStyles(({ token, css }) => ({
`, `,
})); }));
const FileSection: React.FC<FileSectionProps> = ({ const FileSection: React.FC<FileSectionProps> = ({ title, icon, platform, fileType, domainId, appId, downloadingKey, onView, onDownload }) => {
title,
icon,
platform,
fileType,
domainId,
appId,
downloadingKey,
onView,
onDownload,
}) => {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const { styles, cx } = useStyles(); const { styles, cx } = useStyles();
const token = useTheme(); const token = useTheme();
@@ -109,8 +101,7 @@ const FileSection: React.FC<FileSectionProps> = ({
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const dragCounterRef = useRef(0); const dragCounterRef = useRef(0);
const { getSectionFiles, addFile, deleteFile, restoreFile, reorderSection } = const { getSectionFiles, addFile, deleteFile, restoreFile, reorderSection } = useFileChangeStore();
useFileChangeStore();
const files = getSectionFiles(domainId, appId, platform, fileType); const files = getSectionFiles(domainId, appId, platform, fileType);
@@ -135,8 +126,7 @@ const FileSection: React.FC<FileSectionProps> = ({
continue; continue;
} }
const sourcePath = const sourcePath = window.api.getPathForFile(file) || (file as File & { path?: string }).path;
window.api.getPathForFile(file) || (file as File & { path?: string }).path;
if (!sourcePath) { if (!sourcePath) {
message.error(t("fileAddFailed")); message.error(t("fileAddFailed"));
continue; continue;
@@ -177,7 +167,7 @@ const FileSection: React.FC<FileSectionProps> = ({
} }
} }
}, },
[domainId, appId, platform, fileType, addFile, message, t], [domainId, appId, platform, fileType, addFile, message, t]
); );
// ── Drag-and-drop handlers (entire section is the drop zone) ────────────── // ── Drag-and-drop handlers (entire section is the drop zone) ──────────────
@@ -198,18 +188,10 @@ const FileSection: React.FC<FileSectionProps> = ({
const mime = i.type.toLowerCase(); const mime = i.type.toLowerCase();
if (!mime) return false; // unknown → allow, validate on drop if (!mime) return false; // unknown → allow, validate on drop
if (fileType === "js") { if (fileType === "js") {
return !( return !(mime.includes("javascript") || mime.includes("text/plain") || mime === "");
mime.includes("javascript") ||
mime.includes("text/plain") ||
mime === ""
);
} }
if (fileType === "css") { if (fileType === "css") {
return !( return !(mime.includes("css") || mime.includes("text/plain") || mime === "");
mime.includes("css") ||
mime.includes("text/plain") ||
mime === ""
);
} }
return false; return false;
}); });
@@ -218,7 +200,7 @@ const FileSection: React.FC<FileSectionProps> = ({
setIsDraggingOver(true); setIsDraggingOver(true);
} }
}, },
[fileType], [fileType]
); );
const handleDragLeave = useCallback((e: React.DragEvent) => { const handleDragLeave = useCallback((e: React.DragEvent) => {
@@ -247,7 +229,7 @@ const FileSection: React.FC<FileSectionProps> = ({
if (droppedFiles.length === 0) return; if (droppedFiles.length === 0) return;
await saveFile(droppedFiles); await saveFile(droppedFiles);
}, },
[saveFile], [saveFile]
); );
// ── Delete / restore ─────────────────────────────────────────────────────── // ── Delete / restore ───────────────────────────────────────────────────────
@@ -264,29 +246,22 @@ const FileSection: React.FC<FileSectionProps> = ({
} }
deleteFile(domainId, appId, entry.id); deleteFile(domainId, appId, entry.id);
}, },
[domainId, appId, deleteFile, message, t], [domainId, appId, deleteFile, message, t]
); );
const handleRestore = useCallback( const handleRestore = useCallback(
(fileId: string) => { (fileId: string) => {
restoreFile(domainId, appId, fileId); restoreFile(domainId, appId, fileId);
}, },
[domainId, appId, restoreFile], [domainId, appId, restoreFile]
); );
// ── Reorder ──────────────────────────────────────────────────────────────── // ── Reorder ────────────────────────────────────────────────────────────────
const handleReorder = useCallback( const handleReorder = useCallback(
(newOrder: string[], draggedFileId: string) => { (newOrder: string[], draggedFileId: string) => {
reorderSection( reorderSection(domainId, appId, platform, fileType, newOrder, draggedFileId);
domainId,
appId,
platform,
fileType,
newOrder,
draggedFileId
);
}, },
[domainId, appId, platform, fileType, reorderSection], [domainId, appId, platform, fileType, reorderSection]
); );
// ── Render item ──────────────────────────────────────────────────────────── // ── Render item ────────────────────────────────────────────────────────────
@@ -297,27 +272,13 @@ const FileSection: React.FC<FileSectionProps> = ({
entry={entry} entry={entry}
onDelete={() => handleDelete(entry)} onDelete={() => handleDelete(entry)}
onRestore={() => handleRestore(entry.id)} onRestore={() => handleRestore(entry.id)}
onView={ onView={entry.fileKey ? () => onView(entry.fileKey!, entry.fileName) : undefined}
entry.fileKey onDownload={entry.fileKey ? () => onDownload(entry.fileKey!, entry.fileName) : undefined}
? () => onView(entry.fileKey!, entry.fileName)
: undefined
}
onDownload={
entry.fileKey
? () => onDownload(entry.fileKey!, entry.fileName)
: undefined
}
isDownloading={downloadingKey === entry.fileKey} isDownloading={downloadingKey === entry.fileKey}
/> />
); );
}, },
[ [handleDelete, handleRestore, onView, onDownload, downloadingKey]
handleDelete,
handleRestore,
onView,
onDownload,
downloadingKey,
],
); );
// ── Render ───────────────────────────────────────────────────────────────── // ── Render ─────────────────────────────────────────────────────────────────
@@ -350,7 +311,7 @@ const FileSection: React.FC<FileSectionProps> = ({
styles.fileTable, styles.fileTable,
!isDraggingOver && styles.fileTableBorder, !isDraggingOver && styles.fileTableBorder,
isDraggingOver && !isDragInvalid && styles.fileTableDragging, isDraggingOver && !isDragInvalid && styles.fileTableDragging,
isDraggingOver && isDragInvalid && styles.fileTableDraggingInvalid, isDraggingOver && isDragInvalid && styles.fileTableDraggingInvalid
)} )}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -362,15 +323,11 @@ const FileSection: React.FC<FileSectionProps> = ({
<div <div
className={styles.dropOverlay} className={styles.dropOverlay}
style={{ style={{
background: isDragInvalid background: isDragInvalid ? `${token.colorErrorBg}DD` : `${token.colorPrimaryBg}DD`,
? `${token.colorErrorBg}DD`
: `${token.colorPrimaryBg}DD`,
color: isDragInvalid ? token.colorError : token.colorPrimary, color: isDragInvalid ? token.colorError : token.colorPrimary,
}} }}
> >
{isDragInvalid {isDragInvalid ? t("fileTypeNotSupported", { expected: `.${fileType}` }) : t("dropFileHere")}
? t("fileTypeNotSupported", { expected: `.${fileType}` })
: t("dropFileHere")}
</div> </div>
)} )}
@@ -378,20 +335,12 @@ const FileSection: React.FC<FileSectionProps> = ({
{files.length === 0 ? ( {files.length === 0 ? (
<div className={styles.emptySection}>{t("noConfig")}</div> <div className={styles.emptySection}>{t("noConfig")}</div>
) : ( ) : (
<SortableFileList <SortableFileList items={files} onReorder={handleReorder} renderItem={renderItem} />
items={files}
onReorder={handleReorder}
renderItem={renderItem}
/>
)} )}
{/* Click-to-add strip */} {/* Click-to-add strip */}
<div className={styles.dropZoneWrapper}> <div className={styles.dropZoneWrapper}>
<DropZone <DropZone fileType={fileType} isSaving={isSaving} onFileSelected={saveFile} />
fileType={fileType}
isSaving={isSaving}
onFileSelected={saveFile}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,20 +7,8 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { SortableList } from "@lobehub/ui"; import { SortableList } from "@lobehub/ui";
import { import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
DndContext, import { SortableContext, verticalListSortingStrategy, arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable";
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
arrayMove,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers"; import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers";
import type { FileEntry } from "@renderer/stores"; import type { FileEntry } from "@renderer/stores";
@@ -55,12 +43,7 @@ const SortableFileList: React.FC<SortableFileListProps> = ({ items, onReorder, r
); );
return ( return (
<DndContext <DndContext sensors={sensors} collisionDetection={undefined} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}>
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) => (
<SortableList.Item key={entry.id} id={entry.id}> <SortableList.Item key={entry.id} id={entry.id}>

View File

@@ -8,12 +8,7 @@ import { useTranslation } from "react-i18next";
import { Spin, Typography, Space } from "antd"; import { Spin, Typography, Space } from "antd";
import { Button, Tooltip, Empty, Select, Input } from "@lobehub/ui"; import { Button, Tooltip, Empty, Select, Input } from "@lobehub/ui";
import { import { RefreshCw, Search, ArrowUpDown, ArrowDownUp } from "lucide-react";
RefreshCw,
Search,
ArrowUpDown,
ArrowDownUp,
} from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles } from "antd-style";
import { useAppStore } from "@renderer/stores"; import { useAppStore } from "@renderer/stores";
import { useDomainStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
@@ -83,31 +78,10 @@ const AppList: React.FC = () => {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const { styles } = useStyles(); const { styles } = useStyles();
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { const { apps, loading, error, searchText, loadedAt, selectedAppId, setApps, setLoading, setError, setSearchText, setSelectedAppId } = useAppStore();
apps, const { pinnedApps, appSortBy, appSortOrder, togglePinnedApp, setAppSortBy, setAppSortOrder } = useUIStore();
loading,
error,
searchText,
loadedAt,
selectedAppId,
setApps,
setLoading,
setError,
setSearchText,
setSelectedAppId,
} = useAppStore();
const {
pinnedApps,
appSortBy,
appSortOrder,
togglePinnedApp,
setAppSortBy,
setAppSortOrder,
} = useUIStore();
const currentPinnedApps = currentDomain const currentPinnedApps = currentDomain ? pinnedApps[currentDomain.id] || [] : [];
? pinnedApps[currentDomain.id] || []
: [];
// Load apps from Kintone // Load apps from Kintone
const handleLoadApps = async () => { const handleLoadApps = async () => {
@@ -163,10 +137,7 @@ const AppList: React.FC = () => {
if (searchText) { if (searchText) {
const lowerSearch = searchText.toLowerCase(); const lowerSearch = searchText.toLowerCase();
filtered = apps.filter( filtered = apps.filter(
(app) => (app) => app.name.toLowerCase().includes(lowerSearch) || app.appId.includes(searchText) || (app.code && app.code.toLowerCase().includes(lowerSearch))
app.name.toLowerCase().includes(lowerSearch) ||
app.appId.includes(searchText) ||
(app.code && app.code.toLowerCase().includes(lowerSearch)),
); );
} }
@@ -245,30 +216,11 @@ const AppList: React.FC = () => {
]} ]}
style={{ width: 90 }} style={{ width: 90 }}
/> />
<Tooltip <Tooltip title={appSortOrder === "asc" ? t("ascending") : t("descending")}>
title={appSortOrder === "asc" ? t("ascending") : t("descending")} <Button type="text" size="small" icon={appSortOrder === "asc" ? <ArrowUpDown size={16} /> : <ArrowDownUp size={16} />} onClick={toggleSortOrder} />
>
<Button
type="text"
size="small"
icon={
appSortOrder === "asc" ? (
<ArrowUpDown size={16} />
) : (
<ArrowDownUp size={16} />
)
}
onClick={toggleSortOrder}
/>
</Tooltip> </Tooltip>
<Tooltip title={apps.length > 0 ? t("reload") : t("loadApps")}> <Tooltip title={apps.length > 0 ? t("reload") : t("loadApps")}>
<Button <Button type="text" icon={<RefreshCw size={16} />} onClick={handleLoadApps} loading={loading} size="small" />
type="text"
icon={<RefreshCw size={16} />}
onClick={handleLoadApps}
loading={loading}
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</div> </div>
@@ -281,9 +233,7 @@ const AppList: React.FC = () => {
</div> </div>
) : apps.length === 0 ? ( ) : apps.length === 0 ? (
<div className={styles.empty}> <div className={styles.empty}>
<Empty <Empty description={t("noApps")}>
description={t("noApps")}
>
<Button type="primary" onClick={handleLoadApps}> <Button type="primary" onClick={handleLoadApps}>
{t("loadApps")} {t("loadApps")}
</Button> </Button>

View File

@@ -98,14 +98,7 @@ export interface AppListItemProps {
t: (key: string) => string; t: (key: string) => string;
} }
const AppListItem: React.FC<AppListItemProps> = ({ const AppListItem: React.FC<AppListItemProps> = ({ app, isActive, isPinned, onItemClick, onPinToggle, t }) => {
app,
isActive,
isPinned,
onItemClick,
onPinToggle,
t,
}) => {
const { styles } = useStyles(); const { styles } = useStyles();
const token = useTheme(); const token = useTheme();
const [isHovered, setIsHovered] = React.useState(false); const [isHovered, setIsHovered] = React.useState(false);
@@ -127,26 +120,15 @@ const AppListItem: React.FC<AppListItemProps> = ({
<div className={styles.appInfoWrapper}> <div className={styles.appInfoWrapper}>
<div className={styles.iconWrapper}> <div className={styles.iconWrapper}>
{/* Pin overlay - visible on hover for unpinned, always visible for pinned */} {/* Pin overlay - visible on hover for unpinned, always visible for pinned */}
<div <div className={`${styles.pinOverlay} ${showPinOverlay ? styles.pinOverlayVisible : ""}`} onClick={(e) => onPinToggle(e, app.appId)}>
className={`${styles.pinOverlay} ${showPinOverlay ? styles.pinOverlayVisible : ""}`}
onClick={(e) => onPinToggle(e, app.appId)}
>
<Tooltip title={isPinned ? t("unpin") : t("pinApp")}> <Tooltip title={isPinned ? t("unpin") : t("pinApp")}>
<span <span className={`${styles.pinButton} ${isPinned ? styles.pinButtonPinned : ""}`}>
className={`${styles.pinButton} ${isPinned ? styles.pinButtonPinned : ""}`} {isPinned ? <Pin size={16} className="fill-current" /> : <Pin size={16} />}
>
{isPinned ? (
<Pin size={16} className="fill-current" />
) : (
<Pin size={16} />
)}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
{/* App icon - hidden when pin overlay is visible */} {/* App icon - hidden when pin overlay is visible */}
{!showPinOverlay && ( {!showPinOverlay && <LayoutGrid size={16} style={{ color: token.colorLink }} />}
<LayoutGrid size={16} style={{ color: token.colorLink }} />
)}
</div> </div>
<Tooltip title={app.name}> <Tooltip title={app.name}>
<span className={styles.appName}>{app.name}</span> <span className={styles.appName}>{app.name}</span>

View File

@@ -56,15 +56,11 @@ interface CodeViewerProps {
fileType: "js" | "css"; fileType: "js" | "css";
} }
const CodeViewer: React.FC<CodeViewerProps> = ({ const CodeViewer: React.FC<CodeViewerProps> = ({ fileKey, fileName, fileType }) => {
fileKey,
fileName,
fileType,
}) => {
const { t } = useTranslation("file"); const { t } = useTranslation("file");
const { styles } = useStyles(); const { styles } = useStyles();
const { appearance } = useTheme(); const { appearance } = useTheme();
const themeMode = appearance === "dark" ? "dark" : "light" as const; const themeMode = appearance === "dark" ? "dark" : ("light" as const);
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
@@ -158,10 +154,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
let base64 = ""; let base64 = "";
const chunkSize = 8192; const chunkSize = 8192;
for (let i = 0; i < uint8Array.length; i += chunkSize) { for (let i = 0; i < uint8Array.length; i += chunkSize) {
base64 += String.fromCharCode.apply( base64 += String.fromCharCode.apply(null, Array.from(uint8Array.slice(i, i + chunkSize)));
null,
Array.from(uint8Array.slice(i, i + chunkSize)),
);
} }
const base64Content = btoa(base64); const base64Content = btoa(base64);
@@ -174,9 +167,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
if (saveResult.success) { if (saveResult.success) {
message.success(t("downloadSuccess")); message.success(t("downloadSuccess"));
} else { } else {
message.error( message.error(saveResult.error || t("downloadFailed", { ns: "common" }));
saveResult.error || t("downloadFailed", { ns: "common" }),
);
} }
} catch (error) { } catch (error) {
console.error("Download failed:", error); console.error("Download failed:", error);
@@ -225,21 +216,10 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
<div className={styles.header}> <div className={styles.header}>
<span className={styles.fileName}>{fileName}</span> <span className={styles.fileName}>{fileName}</span>
<Space size="small"> <Space size="small">
<Button <Button type="text" size="small" icon={<Copy size={16} />} onClick={handleCopy}>
type="text"
size="small"
icon={<Copy size={16} />}
onClick={handleCopy}
>
{t("copy", { ns: "common" })} {t("copy", { ns: "common" })}
</Button> </Button>
<Button <Button type="text" size="small" icon={<Download size={16} />} loading={downloading} onClick={handleDownload}>
type="text"
size="small"
icon={<Download size={16} />}
loading={downloading}
onClick={handleDownload}
>
{t("download", { ns: "common" })} {t("download", { ns: "common" })}
</Button> </Button>
</Space> </Space>

View File

@@ -42,9 +42,7 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
const { domains, createDomain, updateDomainById } = useDomainStore(); const { domains, createDomain, updateDomainById } = useDomainStore();
const isEdit = !!domainId; const isEdit = !!domainId;
const editingDomain = domainId const editingDomain = domainId ? domains.find((d) => d.id === domainId) : null;
? domains.find((d) => d.id === domainId)
: null;
// Test connection state // Test connection state
const [testing, setTesting] = React.useState(false); const [testing, setTesting] = React.useState(false);
@@ -118,38 +116,31 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
/** /**
* Get form values for domain connection test * Get form values for domain connection test
*/ */
const getConnectionParams = const getConnectionParams = async (): Promise<TestConnectionParams | null> => {
async (): Promise<TestConnectionParams | null> => { try {
try { const values = await form.validateFields(["domain", "username", "password"]);
const values = await form.validateFields([
"domain",
"username",
"password",
]);
const processedDomain = processDomain(values.domain); const processedDomain = processDomain(values.domain);
if (!validateDomainFormat(processedDomain)) { if (!validateDomainFormat(processedDomain)) {
return null;
}
return {
domain: values.domain,
username: values.username,
password: values.password,
processedDomain,
};
} catch {
return null; return null;
} }
};
return {
domain: values.domain,
username: values.username,
password: values.password,
processedDomain,
};
} catch {
return null;
}
};
/** /**
* Test connection with current form values * Test connection with current form values
*/ */
const testConnection = async ( const testConnection = async (params: TestConnectionParams): Promise<TestResult> => {
params: TestConnectionParams,
): Promise<TestResult> => {
if (!params) { if (!params) {
return { success: false, message: t("validDomainRequired") }; return { success: false, message: t("validDomainRequired") };
} }
@@ -257,9 +248,7 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
} else { } else {
// Check for duplicate before creating // Check for duplicate before creating
const existingDomain = domains.find( const existingDomain = domains.find(
(d) => (d) => d.domain.toLowerCase() === processedDomain.toLowerCase() && d.username.toLowerCase() === values.username.toLowerCase()
d.domain.toLowerCase() === processedDomain.toLowerCase() &&
d.username.toLowerCase() === values.username.toLowerCase(),
); );
if (existingDomain) { if (existingDomain) {
@@ -295,11 +284,7 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
const renderTestButton = () => { const renderTestButton = () => {
const getIcon = () => { const getIcon = () => {
if (!testResult) return undefined; if (!testResult) return undefined;
return testResult.success ? ( return testResult.success ? <CheckCircle2 size={16} color={token.colorSuccess} /> : <XCircle size={16} color={token.colorError} />;
<CheckCircle2 size={16} color={token.colorSuccess} />
) : (
<XCircle size={16} color={token.colorError} />
);
}; };
return ( return (
@@ -344,24 +329,12 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
<Input placeholder="https://company.kintone.com" /> <Input placeholder="https://company.kintone.com" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="username" label={t("username")} rules={[{ required: true, message: t("enterUsername") }]}>
name="username"
label={t("username")}
rules={[{ required: true, message: t("enterUsername") }]}
>
<Input placeholder={t("usernameLoginHint")} /> <Input placeholder={t("usernameLoginHint")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="password" label={t("password")} rules={isEdit ? [] : [{ required: true, message: t("enterPassword") }]}>
name="password" <InputPassword placeholder={isEdit ? t("keepPasswordHint") : t("enterPassword")} />
label={t("password")}
rules={
isEdit ? [] : [{ required: true, message: t("enterPassword") }]
}
>
<InputPassword
placeholder={isEdit ? t("keepPasswordHint") : t("enterPassword")}
/>
</Form.Item> </Form.Item>
<Form.Item style={{ marginTop: 32, marginBottom: 0 }}> <Form.Item style={{ marginTop: 32, marginBottom: 0 }}>
@@ -380,17 +353,9 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
{/* Right side: Test button and Create/Update button */} {/* Right side: Test button and Create/Update button */}
<div style={{ display: "flex", alignItems: "center", gap: 8 }}> <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{createError && ( {createError && <span style={{ color: token.colorError, fontSize: 14 }}>{createError.message}</span>}
<span style={{ color: token.colorError, fontSize: 14 }}>
{createError.message}
</span>
)}
{renderTestButton()} {renderTestButton()}
<Button <Button type="primary" onClick={handleSubmit} loading={submitting}>
type="primary"
onClick={handleSubmit}
loading={submitting}
>
{isEdit ? t("update") : t("create")} {isEdit ? t("update") : t("create")}
</Button> </Button>
</div> </div>

View File

@@ -96,8 +96,7 @@ interface DomainListProps {
const DomainList: React.FC<DomainListProps> = ({ onEdit }) => { const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
const { t } = useTranslation("domain"); const { t } = useTranslation("domain");
const { styles } = useStyles(); const { styles } = useStyles();
const { domains, currentDomain, switchDomain, deleteDomain, reorderDomains } = const { domains, currentDomain, switchDomain, deleteDomain, reorderDomains } = useDomainStore();
useDomainStore();
const handleSelect = (domain: Domain) => { const handleSelect = (domain: Domain) => {
if (currentDomain?.id === domain.id) { if (currentDomain?.id === domain.id) {
@@ -137,10 +136,7 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
return ( return (
<SortableList.Item id={domain.id}> <SortableList.Item id={domain.id}>
<div <div className={`${styles.itemWrapper} ${isSelected ? styles.selectedItem : ""}`} onClick={() => handleSelect(domain)}>
className={`${styles.itemWrapper} ${isSelected ? styles.selectedItem : ""}`}
onClick={() => handleSelect(domain)}
>
<div className={styles.itemContent}> <div className={styles.itemContent}>
<SortableList.DragHandle /> <SortableList.DragHandle />
<div className={styles.domainInfo}> <div className={styles.domainInfo}>
@@ -176,13 +172,7 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
cancelText={t("cancel", { ns: "common" })} cancelText={t("cancel", { ns: "common" })}
> >
<Tooltip title={t("delete", { ns: "common" })}> <Tooltip title={t("delete", { ns: "common" })}>
<Button <Button type="text" size="small" danger icon={<Trash2 size={16} />} onClick={(e) => e.stopPropagation()} />
type="text"
size="small"
danger
icon={<Trash2 size={16} />}
onClick={(e) => e.stopPropagation()}
/>
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
</Space> </Space>
@@ -192,14 +182,7 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
); );
}; };
return ( return <SortableList items={domains} renderItem={renderItem} onChange={handleSortChange} gap={4} />;
<SortableList
items={domains}
renderItem={renderItem}
onChange={handleSortChange}
gap={4}
/>
);
}; };
export default DomainList; export default DomainList;

View File

@@ -110,10 +110,7 @@ interface DomainManagerProps {
onToggleCollapse?: () => void; onToggleCollapse?: () => void;
} }
const DomainManager: React.FC<DomainManagerProps> = ({ const DomainManager: React.FC<DomainManagerProps> = ({ collapsed = false, onToggleCollapse }) => {
collapsed = false,
onToggleCollapse,
}) => {
const { t } = useTranslation("domain"); const { t } = useTranslation("domain");
const { styles } = useStyles(); const { styles } = useStyles();
const { domains, loading, loadDomains, currentDomain } = useDomainStore(); const { domains, loading, loadDomains, currentDomain } = useDomainStore();
@@ -144,33 +141,22 @@ const DomainManager: React.FC<DomainManagerProps> = ({
return ( return (
<> <>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<Block <Block direction="horizontal" variant="filled" clickable onClick={onToggleCollapse} className={styles.collapsedBlock}>
direction="horizontal"
variant="filled"
clickable
onClick={onToggleCollapse}
className={styles.collapsedBlock}
>
<div className={styles.collapsedInfo}> <div className={styles.collapsedInfo}>
<Avatar <Avatar size={36} className={styles.collapsedIcon} icon={<Building size={18} />} />
size={36}
className={styles.collapsedIcon}
icon={<Building size={18} />}
/>
<div className={styles.collapsedText}> <div className={styles.collapsedText}>
{currentDomain ? ( {currentDomain ? (
<> <>
<div className={styles.collapsedName}> <div className={styles.collapsedName}>{currentDomain.name}</div>
{currentDomain.name}
</div>
<div className={styles.collapsedDesc}> <div className={styles.collapsedDesc}>
{currentDomain.username} · <a target="_blank" href={"https://" + currentDomain.domain}>{currentDomain.domain}</a> {currentDomain.username} ·{" "}
<a target="_blank" href={"https://" + currentDomain.domain}>
{currentDomain.domain}
</a>
</div> </div>
</> </>
) : ( ) : (
<div className={styles.noDomainText}> <div className={styles.noDomainText}>{t("noDomainSelected")}</div>
{t("noDomainSelected")}
</div>
)} )}
</div> </div>
</div> </div>
@@ -188,11 +174,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
</Block> </Block>
</div> </div>
<DomainForm <DomainForm open={formOpen} onClose={handleCloseForm} domainId={editingDomain} />
open={formOpen}
onClose={handleCloseForm}
domainId={editingDomain}
/>
</> </>
); );
} }
@@ -211,12 +193,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
</Tooltip> </Tooltip>
<div className={styles.headerRight}> <div className={styles.headerRight}>
<Tooltip title={t("addDomain")}> <Tooltip title={t("addDomain")}>
<Button <Button type="primary" size="small" icon={<Plus size={16} />} onClick={handleAdd} />
type="primary"
size="small"
icon={<Plus size={16} />}
onClick={handleAdd}
/>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@@ -234,11 +211,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
</div> </div>
</div> </div>
</div> </div>
<DomainForm <DomainForm open={formOpen} onClose={handleCloseForm} domainId={editingDomain} />
open={formOpen}
onClose={handleCloseForm}
domainId={editingDomain}
/>
</> </>
); );
}; };

View File

@@ -116,20 +116,12 @@ const FileUploader: React.FC<FileUploaderProps> = ({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Dragger <Dragger className={styles.dragger} beforeUpload={handleBeforeUpload} showUploadList={false} multiple accept=".js,.css">
className={styles.dragger}
beforeUpload={handleBeforeUpload}
showUploadList={false}
multiple
accept=".js,.css"
>
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<Inbox size={24} /> <Inbox size={24} />
</p> </p>
<p className="ant-upload-text">{t("clickOrDragToUpload")}</p> <p className="ant-upload-text">{t("clickOrDragToUpload")}</p>
<p className="ant-upload-hint"> <p className="ant-upload-hint">{t("supportFiles", { size: maxFileSize / 1024 / 1024 })}</p>
{t("supportFiles", { size: maxFileSize / 1024 / 1024 })}
</p>
</Dragger> </Dragger>
{files.length > 0 && ( {files.length > 0 && (
@@ -161,29 +153,18 @@ const FileUploader: React.FC<FileUploaderProps> = ({
renderItem={(file, index) => ( renderItem={(file, index) => (
<div className={styles.fileItem}> <div className={styles.fileItem}>
<div className={styles.fileInfo}> <div className={styles.fileInfo}>
<File <File size={20} style={{ color: getFileTypeColor(file.fileType) }} />
size={20}
style={{ color: getFileTypeColor(file.fileType) }}
/>
<div> <div>
<div className={styles.fileName}>{file.fileName}</div> <div className={styles.fileName}>{file.fileName}</div>
<div className={styles.fileSize}> <div className={styles.fileSize}>
{formatFileSize(new Blob([file.content]).size)} {formatFileSize(new Blob([file.content]).size)}
<Tag <Tag color={file.fileType === "js" ? "gold" : "blue"} style={{ marginLeft: 8 }}>
color={file.fileType === "js" ? "gold" : "blue"}
style={{ marginLeft: 8 }}
>
{file.fileType.toUpperCase()} {file.fileType.toUpperCase()}
</Tag> </Tag>
</div> </div>
</div> </div>
</div> </div>
<Button <Button type="text" danger icon={<Trash2 size={16} />} onClick={() => handleRemove(index)} />
type="text"
danger
icon={<Trash2 size={16} />}
onClick={() => handleRemove(index)}
/>
</div> </div>
)} )}
/> />

View File

@@ -159,13 +159,7 @@ const Settings: React.FC = () => {
{t("language")} {t("language")}
</Title> </Title>
</div> </div>
<Radio.Group <Radio.Group value={locale} onChange={(e) => handleLocaleChange(e.target.value)} optionType="button" buttonStyle="solid" className={styles.radioGroup}>
value={locale}
onChange={(e) => handleLocaleChange(e.target.value)}
optionType="button"
buttonStyle="solid"
className={styles.radioGroup}
>
{LOCALES.map((localeConfig) => ( {LOCALES.map((localeConfig) => (
<Radio.Button key={localeConfig.code} value={localeConfig.code}> <Radio.Button key={localeConfig.code} value={localeConfig.code}>
{localeConfig.nativeName} {localeConfig.nativeName}
@@ -219,33 +213,18 @@ const Settings: React.FC = () => {
</div> </div>
<Space direction="vertical" style={{ width: "100%", marginTop: 12 }}> <Space direction="vertical" style={{ width: "100%", marginTop: 12 }}>
<Button <Button type="primary" icon={<ExternalLink size={14} />} onClick={handleCheckUpdate} loading={checkingUpdate}>
type="primary"
icon={<ExternalLink size={14} />}
onClick={handleCheckUpdate}
loading={checkingUpdate}
>
{t("checkUpdate")} {t("checkUpdate")}
</Button> </Button>
{updateInfo && ( {updateInfo && (
<div <div className={`${styles.updateInfo} ${updateInfo.hasUpdate ? styles.updateAvailable : ""}`}>
className={`${styles.updateInfo} ${
updateInfo.hasUpdate ? styles.updateAvailable : ""
}`}
>
{updateInfo.hasUpdate ? ( {updateInfo.hasUpdate ? (
<Space direction="vertical" size={4}> <Space direction="vertical" size={4}>
<span> <span>
{t("updateAvailable")}: v{updateInfo.version} {t("updateAvailable")}: v{updateInfo.version}
</span> </span>
<Button <Button type="link" size="small" icon={<ExternalLink size={12} />} onClick={handleOpenReleasePage} style={{ padding: 0, height: "auto" }}>
type="link"
size="small"
icon={<ExternalLink size={12} />}
onClick={handleOpenReleasePage}
style={{ padding: 0, height: "auto" }}
>
{t("downloadUpdate")} {t("downloadUpdate")}
</Button> </Button>
</Space> </Space>

View File

@@ -5,24 +5,10 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { List, Tag, Space, Spin, Popconfirm, Typography } from "antd";
List,
Tag,
Space,
Spin,
Popconfirm,
Typography,
} from "antd";
import { Button, Tooltip, Empty, Avatar } from "@lobehub/ui"; import { Button, Tooltip, Empty, Avatar } from "@lobehub/ui";
import { import { History, Download, Trash2, Undo2, Code, FileText } from "lucide-react";
History,
Download,
Trash2,
Undo2,
Code,
FileText,
} from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles } from "antd-style";
import { useVersionStore } from "@renderer/stores"; import { useVersionStore } from "@renderer/stores";
import { useDomainStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
@@ -104,8 +90,7 @@ const VersionHistory: React.FC = () => {
const { styles } = useStyles(); const { styles } = useStyles();
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { currentApp } = useAppStore(); const { currentApp } = useAppStore();
const { versions, loading, setVersions, setLoading, removeVersion } = const { versions, loading, setVersions, setLoading, removeVersion } = useVersionStore();
useVersionStore();
// Load versions when app changes // Load versions when app changes
React.useEffect(() => { React.useEffect(() => {
@@ -191,9 +176,7 @@ const VersionHistory: React.FC = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div style={{ padding: 24, textAlign: "center" }}> <div style={{ padding: 24, textAlign: "center" }}>
<Empty <Empty description={t("selectApp", { ns: "app" })} />
description={t("selectApp", { ns: "app" })}
/>
</div> </div>
</div> </div>
); );
@@ -215,20 +198,14 @@ const VersionHistory: React.FC = () => {
<Text strong>{t("title")}</Text> <Text strong>{t("title")}</Text>
<Tag>{t("totalVersions", { count: versions.length })}</Tag> <Tag>{t("totalVersions", { count: versions.length })}</Tag>
</div> </div>
<Button <Button icon={<Download size={16} />} onClick={loadVersions} loading={loading}>
icon={<Download size={16} />}
onClick={loadVersions}
loading={loading}
>
{t("refresh", { ns: "common" })} {t("refresh", { ns: "common" })}
</Button> </Button>
</div> </div>
<div className={styles.content}> <div className={styles.content}>
{versions.length === 0 ? ( {versions.length === 0 ? (
<Empty <Empty description={t("noVersions")} />
description={t("noVersions")}
/>
) : ( ) : (
<List <List
dataSource={versions} dataSource={versions}
@@ -238,16 +215,9 @@ const VersionHistory: React.FC = () => {
<div className={styles.versionItem}> <div className={styles.versionItem}>
<div className={styles.versionInfo}> <div className={styles.versionInfo}>
<Avatar <Avatar
icon={ icon={version.fileType === "js" ? <Code size={16} /> : <FileText size={16} />}
version.fileType === "js" ? (
<Code size={16} />
) : (
<FileText size={16} />
)
}
style={{ style={{
backgroundColor: backgroundColor: version.fileType === "js" ? "#f7df1e" : "#264de4",
version.fileType === "js" ? "#f7df1e" : "#264de4",
}} }}
/> />
<div className={styles.versionDetails}> <div className={styles.versionDetails}>
@@ -255,45 +225,28 @@ const VersionHistory: React.FC = () => {
<div className={styles.versionMeta}> <div className={styles.versionMeta}>
<Tag color={sourceTag.color}>{sourceTag.text}</Tag> <Tag color={sourceTag.color}>{sourceTag.text}</Tag>
<Tag>{version.fileType.toUpperCase()}</Tag> <Tag>{version.fileType.toUpperCase()}</Tag>
<Text type="secondary"> <Text type="secondary">{formatFileSize(version.size)}</Text>
{formatFileSize(version.size)} <Text type="secondary">{formatDate(version.createdAt)}</Text>
</Text>
<Text type="secondary">
{formatDate(version.createdAt)}
</Text>
</div> </div>
{version.tags && version.tags.length > 0 && ( {version.tags && version.tags.length > 0 && (
<div className={styles.tags}> <div className={styles.tags}>
{version.tags.map((tag, i) => ( {version.tags.map((tag, i) => (
<Tag <Tag key={i} color="processing">
key={i}
color="processing"
>
{tag} {tag}
</Tag> </Tag>
))} ))}
</div> </div>
)} )}
{version.notes && ( {version.notes && <Text type="secondary">{version.notes}</Text>}
<Text type="secondary">{version.notes}</Text>
)}
</div> </div>
</div> </div>
<Space> <Space>
<Tooltip title={t("viewCode")}> <Tooltip title={t("viewCode")}>
<Button <Button type="text" size="small" icon={<Code size={16} />} />
type="text"
size="small"
icon={<Code size={16} />}
/>
</Tooltip> </Tooltip>
<Tooltip title={t("download", { ns: "common" })}> <Tooltip title={t("download", { ns: "common" })}>
<Button <Button type="text" size="small" icon={<Download size={16} />} />
type="text"
size="small"
icon={<Download size={16} />}
/>
</Tooltip> </Tooltip>
<Tooltip title={t("confirmRollback")}> <Tooltip title={t("confirmRollback")}>
<Popconfirm <Popconfirm
@@ -303,11 +256,7 @@ const VersionHistory: React.FC = () => {
okText={t("sourceRollback")} okText={t("sourceRollback")}
cancelText={t("cancel", { ns: "common" })} cancelText={t("cancel", { ns: "common" })}
> >
<Button <Button type="text" size="small" icon={<Undo2 size={16} />} />
type="text"
size="small"
icon={<Undo2 size={16} />}
/>
</Popconfirm> </Popconfirm>
</Tooltip> </Tooltip>
<Popconfirm <Popconfirm
@@ -317,12 +266,7 @@ const VersionHistory: React.FC = () => {
okText={t("delete", { ns: "common" })} okText={t("delete", { ns: "common" })}
cancelText={t("cancel", { ns: "common" })} cancelText={t("cancel", { ns: "common" })}
> >
<Button <Button type="text" size="small" danger icon={<Trash2 size={16} />} />
type="text"
size="small"
danger
icon={<Trash2 size={16} />}
/>
</Popconfirm> </Popconfirm>
</Space> </Space>
</div> </div>

View File

@@ -1,10 +1,10 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly MAIN_VITE_API_URL: string readonly MAIN_VITE_API_URL: string;
readonly MAIN_VITE_DEBUG: string readonly MAIN_VITE_DEBUG: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv;
} }

View File

@@ -4,9 +4,9 @@ body,
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, font-family:
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
'Noto Color Emoji'; "Segoe UI Symbol", "Noto Color Emoji";
} }
/* macOS style window controls area */ /* macOS style window controls area */

View File

@@ -7,7 +7,7 @@ import i18n from "./i18n";
import App from "./App"; import App from "./App";
import { useThemeStore } from "./stores/themeStore"; import { useThemeStore } from "./stores/themeStore";
import { motion } from 'motion/react'; import { motion } from "motion/react";
const ThemeApp: React.FC = () => { const ThemeApp: React.FC = () => {
const { themeMode, setThemeMode } = useThemeStore(); const { themeMode, setThemeMode } = useThemeStore();
@@ -17,8 +17,8 @@ const ThemeApp: React.FC = () => {
themeMode={themeMode} themeMode={themeMode}
onThemeModeChange={setThemeMode} onThemeModeChange={setThemeMode}
// customTheme={{primaryColor: 'blue'}} // customTheme={{primaryColor: 'blue'}}
customToken={ () => ({ customToken={() => ({
colorLink: '#1890ff' colorLink: "#1890ff",
})} })}
> >
<AntdApp> <AntdApp>
@@ -35,5 +35,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<ThemeApp /> <ThemeApp />
</I18nextProvider> </I18nextProvider>
</ConfigProvider> </ConfigProvider>
</React.StrictMode>, </React.StrictMode>
); );

View File

@@ -100,6 +100,6 @@ export const useAppStore = create<AppState>()(
loadedAt: state.loadedAt, loadedAt: state.loadedAt,
selectedAppId: state.selectedAppId, selectedAppId: state.selectedAppId,
}), }),
}, }
), )
); );

View File

@@ -26,11 +26,7 @@ interface DomainState {
removeDomain: (id: string) => void; removeDomain: (id: string) => void;
reorderDomains: (fromIndex: number, toIndex: number) => void; reorderDomains: (fromIndex: number, toIndex: number) => void;
setCurrentDomain: (domain: Domain | null) => void; setCurrentDomain: (domain: Domain | null) => void;
setConnectionStatus: ( setConnectionStatus: (id: string, status: ConnectionStatus, error?: string) => void;
id: string,
status: ConnectionStatus,
error?: string,
) => void;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
setError: (error: string | null) => void; setError: (error: string | null) => void;
clearError: () => void; clearError: () => void;
@@ -66,17 +62,13 @@ export const useDomainStore = create<DomainState>()(
updateDomain: (domain) => updateDomain: (domain) =>
set((state) => ({ set((state) => ({
domains: state.domains.map((d) => (d.id === domain.id ? domain : d)), domains: state.domains.map((d) => (d.id === domain.id ? domain : d)),
currentDomain: currentDomain: state.currentDomain?.id === domain.id ? domain : state.currentDomain,
state.currentDomain?.id === domain.id
? domain
: state.currentDomain,
})), })),
removeDomain: (id) => removeDomain: (id) =>
set((state) => ({ set((state) => ({
domains: state.domains.filter((d) => d.id !== id), domains: state.domains.filter((d) => d.id !== id),
currentDomain: currentDomain: state.currentDomain?.id === id ? null : state.currentDomain,
state.currentDomain?.id === id ? null : state.currentDomain,
})), })),
reorderDomains: (fromIndex, toIndex) => reorderDomains: (fromIndex, toIndex) =>
@@ -92,9 +84,7 @@ export const useDomainStore = create<DomainState>()(
setConnectionStatus: (id, status, error) => setConnectionStatus: (id, status, error) =>
set((state) => ({ set((state) => ({
connectionStatuses: { ...state.connectionStatuses, [id]: status }, connectionStatuses: { ...state.connectionStatuses, [id]: status },
connectionErrors: error connectionErrors: error ? { ...state.connectionErrors, [id]: error } : state.connectionErrors,
? { ...state.connectionErrors, [id]: error }
: state.connectionErrors,
})), })),
setLoading: (loading) => set({ loading }), setLoading: (loading) => set({ loading }),
@@ -115,8 +105,7 @@ export const useDomainStore = create<DomainState>()(
} }
} catch (error) { } catch (error) {
set({ set({
error: error: error instanceof Error ? error.message : "Failed to load domains",
error instanceof Error ? error.message : "Failed to load domains",
loading: false, loading: false,
}); });
} }
@@ -124,9 +113,7 @@ export const useDomainStore = create<DomainState>()(
createDomain: async (params: CreateDomainParams) => { createDomain: async (params: CreateDomainParams) => {
// Check for duplicate domain // Check for duplicate domain
const existingDomain = get().domains.find( const existingDomain = get().domains.find((d) => d.domain === params.domain && d.username === params.username);
(d) => d.domain === params.domain && d.username === params.username,
);
if (existingDomain) { if (existingDomain) {
set({ error: "domainAlreadyExists", loading: false }); set({ error: "domainAlreadyExists", loading: false });
return false; return false;
@@ -147,10 +134,7 @@ export const useDomainStore = create<DomainState>()(
} }
} catch (error) { } catch (error) {
set({ set({
error: error: error instanceof Error ? error.message : "Failed to create domain",
error instanceof Error
? error.message
: "Failed to create domain",
loading: false, loading: false,
}); });
return false; return false;
@@ -163,13 +147,8 @@ export const useDomainStore = create<DomainState>()(
const result = await window.api.updateDomain(params); const result = await window.api.updateDomain(params);
if (result.success) { if (result.success) {
set((state) => ({ set((state) => ({
domains: state.domains.map((d) => domains: state.domains.map((d) => (d.id === result.data.id ? result.data : d)),
d.id === result.data.id ? result.data : d, currentDomain: state.currentDomain?.id === result.data.id ? result.data : state.currentDomain,
),
currentDomain:
state.currentDomain?.id === result.data.id
? result.data
: state.currentDomain,
loading: false, loading: false,
})); }));
return true; return true;
@@ -179,10 +158,7 @@ export const useDomainStore = create<DomainState>()(
} }
} catch (error) { } catch (error) {
set({ set({
error: error: error instanceof Error ? error.message : "Failed to update domain",
error instanceof Error
? error.message
: "Failed to update domain",
loading: false, loading: false,
}); });
return false; return false;
@@ -196,8 +172,7 @@ export const useDomainStore = create<DomainState>()(
if (result.success) { if (result.success) {
set((state) => ({ set((state) => ({
domains: state.domains.filter((d) => d.id !== id), domains: state.domains.filter((d) => d.id !== id),
currentDomain: currentDomain: state.currentDomain?.id === id ? null : state.currentDomain,
state.currentDomain?.id === id ? null : state.currentDomain,
loading: false, loading: false,
})); }));
return true; return true;
@@ -207,10 +182,7 @@ export const useDomainStore = create<DomainState>()(
} }
} catch (error) { } catch (error) {
set({ set({
error: error: error instanceof Error ? error.message : "Failed to delete domain",
error instanceof Error
? error.message
: "Failed to delete domain",
loading: false, loading: false,
}); });
return false; return false;
@@ -271,9 +243,7 @@ export const useDomainStore = create<DomainState>()(
...state.connectionStatuses, ...state.connectionStatuses,
[id]: status.connectionStatus, [id]: status.connectionStatus,
}, },
connectionErrors: status.connectionError connectionErrors: status.connectionError ? { ...state.connectionErrors, [id]: status.connectionError } : state.connectionErrors,
? { ...state.connectionErrors, [id]: status.connectionError }
: state.connectionErrors,
})); }));
return status; return status;
} else { } else {
@@ -294,10 +264,7 @@ export const useDomainStore = create<DomainState>()(
connectionStatuses: { ...state.connectionStatuses, [id]: "error" }, connectionStatuses: { ...state.connectionStatuses, [id]: "error" },
connectionErrors: { connectionErrors: {
...state.connectionErrors, ...state.connectionErrors,
[id]: [id]: error instanceof Error ? error.message : "Connection test failed",
error instanceof Error
? error.message
: "Connection test failed",
}, },
})); }));
return null; return null;
@@ -310,6 +277,6 @@ export const useDomainStore = create<DomainState>()(
domains: state.domains, domains: state.domains,
currentDomain: state.currentDomain, currentDomain: state.currentDomain,
}), }),
}, }
), )
); );

View File

@@ -41,11 +41,7 @@ interface FileChangeState {
* No-op if the app is already initialized (has pending changes). * No-op if the app is already initialized (has pending changes).
* Call clearChanges() first to force re-initialization. * Call clearChanges() first to force re-initialization.
*/ */
initializeApp: ( initializeApp: (domainId: string, appId: string, files: Array<Omit<FileEntry, "status">>) => void;
domainId: string,
appId: string,
files: Array<Omit<FileEntry, "status">>,
) => void;
/** /**
* Add a new locally-staged file (status: added). * Add a new locally-staged file (status: added).
@@ -70,14 +66,7 @@ interface FileChangeState {
* Reorder files within a specific (platform, fileType) section. * Reorder files within a specific (platform, fileType) section.
* The dragged file's status will be set to "reordered" (if it was "unchanged"). * The dragged file's status will be set to "reordered" (if it was "unchanged").
*/ */
reorderSection: ( reorderSection: (domainId: string, appId: string, platform: "desktop" | "mobile", fileType: "js" | "css", newOrder: string[], draggedFileId: string) => void;
domainId: string,
appId: string,
platform: "desktop" | "mobile",
fileType: "js" | "css",
newOrder: string[],
draggedFileId: string
) => void;
/** /**
* Clear all pending changes and reset initialized state. * Clear all pending changes and reset initialized state.
@@ -89,18 +78,10 @@ interface FileChangeState {
getFiles: (domainId: string, appId: string) => FileEntry[]; getFiles: (domainId: string, appId: string) => FileEntry[];
/** Get files for a specific section */ /** Get files for a specific section */
getSectionFiles: ( getSectionFiles: (domainId: string, appId: string, platform: "desktop" | "mobile", fileType: "js" | "css") => FileEntry[];
domainId: string,
appId: string,
platform: "desktop" | "mobile",
fileType: "js" | "css",
) => FileEntry[];
/** Count of added, deleted, and reordered files */ /** Count of added, deleted, and reordered files */
getChangeCount: ( getChangeCount: (domainId: string, appId: string) => { added: number; deleted: number; reordered: number };
domainId: string,
appId: string
) => { added: number; deleted: number; reordered: number };
isInitialized: (domainId: string, appId: string) => boolean; isInitialized: (domainId: string, appId: string) => boolean;
} }
@@ -175,9 +156,7 @@ export const useFileChangeStore = create<FileChangeState>()(
updatedFiles = existing.files.filter((f) => f.id !== fileId); updatedFiles = existing.files.filter((f) => f.id !== fileId);
} else if (file.status === "unchanged" || file.status === "reordered") { } else if (file.status === "unchanged" || file.status === "reordered") {
// Mark unchanged/reordered files as deleted // Mark unchanged/reordered files as deleted
updatedFiles = existing.files.map((f) => updatedFiles = existing.files.map((f) => (f.id === fileId ? { ...f, status: "deleted" as FileStatus } : f));
f.id === fileId ? { ...f, status: "deleted" as FileStatus } : f,
);
} else { } else {
return state; return state;
} }
@@ -202,11 +181,7 @@ export const useFileChangeStore = create<FileChangeState>()(
...state.appFiles, ...state.appFiles,
[key]: { [key]: {
...existing, ...existing,
files: existing.files.map((f) => files: existing.files.map((f) => (f.id === fileId && f.status === "deleted" ? { ...f, status: "unchanged" as FileStatus } : f)),
f.id === fileId && f.status === "deleted"
? { ...f, status: "unchanged" as FileStatus }
: f,
),
}, },
}, },
}; };
@@ -220,18 +195,12 @@ export const useFileChangeStore = create<FileChangeState>()(
if (!existing) return state; if (!existing) return state;
// Split files into this section and others // Split files into this section and others
const sectionFiles = existing.files.filter( const sectionFiles = existing.files.filter((f) => f.platform === platform && f.fileType === fileType);
(f) => f.platform === platform && f.fileType === fileType, const otherFiles = existing.files.filter((f) => !(f.platform === platform && f.fileType === fileType));
);
const otherFiles = existing.files.filter(
(f) => !(f.platform === platform && f.fileType === fileType),
);
// Reorder section files according to newOrder // Reorder section files according to newOrder
const sectionMap = new Map(sectionFiles.map((f) => [f.id, f])); const sectionMap = new Map(sectionFiles.map((f) => [f.id, f]));
const reordered = newOrder const reordered = newOrder.map((id) => sectionMap.get(id)).filter((f): f is FileEntry => f !== undefined);
.map((id) => sectionMap.get(id))
.filter((f): f is FileEntry => f !== undefined);
// Append any section files not in newOrder (safety) // Append any section files not in newOrder (safety)
for (const f of sectionFiles) { for (const f of sectionFiles) {
@@ -279,9 +248,7 @@ export const useFileChangeStore = create<FileChangeState>()(
getSectionFiles: (domainId, appId, platform, fileType) => { getSectionFiles: (domainId, appId, platform, fileType) => {
const key = appKey(domainId, appId); const key = appKey(domainId, appId);
const files = get().appFiles[key]?.files ?? []; const files = get().appFiles[key]?.files ?? [];
return files.filter( return files.filter((f) => f.platform === platform && f.fileType === fileType);
(f) => f.platform === platform && f.fileType === fileType,
);
}, },
getChangeCount: (domainId, appId) => { getChangeCount: (domainId, appId) => {

View File

@@ -29,6 +29,6 @@ export const useLocaleStore = create<LocaleState>()(
partialize: (state) => ({ partialize: (state) => ({
locale: state.locale, locale: state.locale,
}), }),
}, }
), )
); );

View File

@@ -49,6 +49,6 @@ export const useSessionStore = create<SessionState>()(
viewMode: state.viewMode, viewMode: state.viewMode,
selectedFile: state.selectedFile, selectedFile: state.selectedFile,
}), }),
}, }
), )
); );

View File

@@ -27,6 +27,6 @@ export const useThemeStore = create<ThemeState>()(
}), }),
{ {
name: "theme-storage", name: "theme-storage",
}, }
), )
); );

View File

@@ -50,10 +50,7 @@ export const useUIStore = create<UIState>()(
// Actions // Actions
setSidebarWidth: (width) => setSidebarWidth: (width) =>
set({ set({
sidebarWidth: Math.min( sidebarWidth: Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, width)),
MAX_SIDEBAR_WIDTH,
Math.max(MIN_SIDEBAR_WIDTH, width),
),
}), }),
setSiderCollapsed: (collapsed) => set({ siderCollapsed: collapsed }), setSiderCollapsed: (collapsed) => set({ siderCollapsed: collapsed }),
@@ -74,9 +71,7 @@ export const useUIStore = create<UIState>()(
set((state) => { set((state) => {
const currentPinned = state.pinnedApps[domainId] || []; const currentPinned = state.pinnedApps[domainId] || [];
const isPinned = currentPinned.includes(appId); const isPinned = currentPinned.includes(appId);
const newPinned = isPinned const newPinned = isPinned ? currentPinned.filter((id) => id !== appId) : [appId, ...currentPinned];
? currentPinned.filter((id) => id !== appId)
: [appId, ...currentPinned];
return { return {
pinnedApps: { ...state.pinnedApps, [domainId]: newPinned }, pinnedApps: { ...state.pinnedApps, [domainId]: newPinned },
}; };
@@ -97,6 +92,6 @@ export const useUIStore = create<UIState>()(
appSortBy: state.appSortBy, appSortBy: state.appSortBy,
appSortOrder: state.appSortOrder, appSortOrder: state.appSortOrder,
}), }),
}, }
), )
); );

View File

@@ -45,8 +45,7 @@ export const useVersionStore = create<VersionState>()((set) => ({
removeVersion: (id) => removeVersion: (id) =>
set((state) => ({ set((state) => ({
versions: state.versions.filter((v) => v.id !== id), versions: state.versions.filter((v) => v.id !== id),
selectedVersion: selectedVersion: state.selectedVersion?.id === id ? null : state.selectedVersion,
state.selectedVersion?.id === id ? null : state.selectedVersion,
})), })),
setSelectedVersion: (version) => set({ selectedVersion: version }), setSelectedVersion: (version) => set({ selectedVersion: version }),

View File

@@ -8,9 +8,7 @@ import type { AppResponse, AppDetail, FileContent } from "./kintone";
import type { Version, DownloadMetadata, BackupMetadata } from "./version"; import type { Version, DownloadMetadata, BackupMetadata } from "./version";
// Unified result type // Unified result type
export type Result<T> = export type Result<T> = { success: true; data: T } | { success: false; error: string };
| { success: true; data: T }
| { success: false; error: string };
// ==================== Domain IPC Types ==================== // ==================== Domain IPC Types ====================
@@ -193,16 +191,12 @@ export interface ElectronAPI {
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>; updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
deleteDomain: (id: string) => Promise<Result<void>>; deleteDomain: (id: string) => Promise<Result<void>>;
testConnection: (id: string) => Promise<Result<DomainWithStatus>>; testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
testDomainConnection: ( testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
params: TestDomainConnectionParams,
) => Promise<Result<boolean>>;
// Browse // Browse
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: ( getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
params: GetFileContentParams,
) => Promise<Result<FileContent>>;
// Deploy // Deploy
deploy: (params: DeployParams) => Promise<Result<DeployResult>>; deploy: (params: DeployParams) => Promise<Result<DeployResult>>;

View File

@@ -22,14 +22,10 @@ type KintoneClient = KintoneRestAPIClient;
export type AppResponse = Awaited<ReturnType<KintoneClient["app"]["getApp"]>>; export type AppResponse = Awaited<ReturnType<KintoneClient["app"]["getApp"]>>;
/** App customization request - parameters for updateAppCustomize */ /** App customization request - parameters for updateAppCustomize */
export type AppCustomizeParameter = Parameters< export type AppCustomizeParameter = Parameters<KintoneClient["app"]["updateAppCustomize"]>[number];
KintoneClient["app"]["updateAppCustomize"]
>[number];
/** App customization response */ /** App customization response */
export type AppCustomizeResponse = Awaited< export type AppCustomizeResponse = Awaited<ReturnType<KintoneClient["app"]["getAppCustomize"]>>;
ReturnType<KintoneClient["app"]["getAppCustomize"]>
>;
// ============== Custom Business Types ============== // ============== Custom Business Types ==============
@@ -53,7 +49,6 @@ export interface FileContent {
content?: string; // Base64 encoded or text content?: string; // Base64 encoded or text
} }
/** /**
* File config for customization * File config for customization
* Using SDK's type directly from AppCustomizeResponse * Using SDK's type directly from AppCustomizeResponse
@@ -75,16 +70,13 @@ type ExtractFileType<T> = T extends { type: "FILE" } ? T : never;
export type FileResourceParameter = ExtractFileType<FileConfigParameter>; export type FileResourceParameter = ExtractFileType<FileConfigParameter>;
export type FileResourceResponse = ExtractFileType<FileConfigResponse>; export type FileResourceResponse = ExtractFileType<FileConfigResponse>;
// ============== Type Guards ============== // ============== Type Guards ==============
/** /**
* Check if resource is URL type - works with both Response and Parameter types * Check if resource is URL type - works with both Response and Parameter types
* TypeScript will automatically narrow the type based on usage * TypeScript will automatically narrow the type based on usage
*/ */
export function isUrlResource( export function isUrlResource(resource: FileConfigResponse | FileConfigParameter): resource is UrlResourceParameter | UrlResourceResponse {
resource: FileConfigResponse | FileConfigParameter,
): resource is UrlResourceParameter | UrlResourceResponse {
return resource.type === "URL" && !!resource.url; return resource.type === "URL" && !!resource.url;
} }
@@ -92,8 +84,6 @@ export function isUrlResource(
* Check if resource is FILE type - works with both Response and Parameter types * Check if resource is FILE type - works with both Response and Parameter types
* TypeScript will automatically narrow the type based on usage * TypeScript will automatically narrow the type based on usage
*/ */
export function isFileResource( export function isFileResource(resource: FileConfigResponse | FileConfigParameter): resource is FileResourceParameter | FileResourceResponse {
resource: FileConfigResponse | FileConfigParameter,
): resource is FileResourceParameter | FileResourceResponse {
return resource.type === "FILE" && !!resource.file?.fileKey; return resource.type === "FILE" && !!resource.file?.fileKey;
} }

View File

@@ -3,11 +3,7 @@ import { isFileResource, isUrlResource, type FileConfigResponse } from "@shared/
/** /**
* Get user-friendly display name for a file config * Get user-friendly display name for a file config
*/ */
export function getDisplayName( export function getDisplayName(file: FileConfigResponse, fileType: "js" | "css", index: number): string {
file: FileConfigResponse,
fileType: "js" | "css",
index: number,
): string {
if (isUrlResource(file)) { if (isUrlResource(file)) {
return extractFilenameFromUrl(file.url) || `URL ${index + 1}`; return extractFilenameFromUrl(file.url) || `URL ${index + 1}`;
} }

View File

@@ -1,7 +1,4 @@
import type { import type { AppCustomizeResponse, FileConfigResponse } from "@shared/types/kintone";
AppCustomizeResponse,
FileConfigResponse,
} from "@shared/types/kintone";
import { isFileResource, isUrlResource } from "@shared/types/kintone"; import { isFileResource, isUrlResource } from "@shared/types/kintone";
import type { FileEntry } from "@renderer/stores/fileChangeStore"; import type { FileEntry } from "@renderer/stores/fileChangeStore";
import { getDisplayName, getFileKey } from "./fileDisplay"; import { getDisplayName, getFileKey } from "./fileDisplay";
@@ -10,47 +7,29 @@ import { getDisplayName, getFileKey } from "./fileDisplay";
* Transform Kintone customize data into FileEntry array format * Transform Kintone customize data into FileEntry array format
* Used to initialize file change store from Kintone API response * Used to initialize file change store from Kintone API response
*/ */
export function transformCustomizeToFiles( export function transformCustomizeToFiles(customize: AppCustomizeResponse | undefined): Array<Omit<FileEntry, "status">> {
customize: AppCustomizeResponse | undefined,
): Array<Omit<FileEntry, "status">> {
if (!customize) return []; if (!customize) return [];
const files: Array<Omit<FileEntry, "status">> = []; const files: Array<Omit<FileEntry, "status">> = [];
// Desktop JS files // Desktop JS files
if (customize.desktop?.js) { if (customize.desktop?.js) {
files.push( files.push(...customize.desktop.js.map((file, index) => transformFileConfig(file, "js", "desktop", index)));
...customize.desktop.js.map((file, index) =>
transformFileConfig(file, "js", "desktop", index),
),
);
} }
// Desktop CSS files // Desktop CSS files
if (customize.desktop?.css) { if (customize.desktop?.css) {
files.push( files.push(...customize.desktop.css.map((file, index) => transformFileConfig(file, "css", "desktop", index)));
...customize.desktop.css.map((file, index) =>
transformFileConfig(file, "css", "desktop", index),
),
);
} }
// Mobile JS files // Mobile JS files
if (customize.mobile?.js) { if (customize.mobile?.js) {
files.push( files.push(...customize.mobile.js.map((file, index) => transformFileConfig(file, "js", "mobile", index)));
...customize.mobile.js.map((file, index) =>
transformFileConfig(file, "js", "mobile", index),
),
);
} }
// Mobile CSS files // Mobile CSS files
if (customize.mobile?.css) { if (customize.mobile?.css) {
files.push( files.push(...customize.mobile.css.map((file, index) => transformFileConfig(file, "css", "mobile", index)));
...customize.mobile.css.map((file, index) =>
transformFileConfig(file, "css", "mobile", index),
),
);
} }
return files; return files;
@@ -59,20 +38,13 @@ export function transformCustomizeToFiles(
/** /**
* Transform a single file config into FileEntry format * Transform a single file config into FileEntry format
*/ */
function transformFileConfig( function transformFileConfig(file: FileConfigResponse, fileType: "js" | "css", platform: "desktop" | "mobile", index: number): Omit<FileEntry, "status"> {
file: FileConfigResponse,
fileType: "js" | "css",
platform: "desktop" | "mobile",
index: number,
): Omit<FileEntry, "status"> {
return { return {
id: crypto.randomUUID(), id: crypto.randomUUID(),
fileName: getDisplayName(file, fileType, index), fileName: getDisplayName(file, fileType, index),
fileType, fileType,
platform, platform,
size: isFileResource(file) size: isFileResource(file) ? parseInt(file.file.size ?? "0", 10) || undefined : undefined,
? parseInt(file.file.size ?? "0", 10) || undefined
: undefined,
fileKey: getFileKey(file), fileKey: getFileKey(file),
url: isUrlResource(file) ? file.url : undefined, url: isUrlResource(file) ? file.url : undefined,
}; };