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,
"trailingComma": "es5",
"printWidth": 200,
"printWidth": 160,
"semi": true,
"singleQuote": false
}

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

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

View File

@@ -27,8 +27,7 @@ export type MainErrorKey =
const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
"zh-CN": {
domainNotFound: "域名未找到",
domainDuplicate:
'域名 "{{domain}}" 与用户 "{{username}}" 已存在。请编辑现有域名。',
domainDuplicate: '域名 "{{domain}}" 与用户 "{{username}}" 已存在。请编辑现有域名。',
connectionFailed: "连接失败",
unknownError: "未知错误",
rollbackNotImplemented: "回滚功能尚未实现",
@@ -40,8 +39,7 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
},
"en-US": {
domainNotFound: "Domain not found",
domainDuplicate:
'Domain "{{domain}}" with user "{{username}}" already exists. Please edit the existing domain instead.',
domainDuplicate: 'Domain "{{domain}}" with user "{{username}}" already exists. Please edit the existing domain instead.',
connectionFailed: "Connection failed",
unknownError: "Unknown error",
rollbackNotImplemented: "Rollback not yet implemented",
@@ -53,8 +51,7 @@ const errorMessages: Record<LocaleCode, Record<MainErrorKey, string>> = {
},
"ja-JP": {
domainNotFound: "ドメインが見つかりません",
domainDuplicate:
'ドメイン "{{domain}}" とユーザー "{{username}}" は既に存在します。既存のドメインを編集してください。',
domainDuplicate: 'ドメイン "{{domain}}" とユーザー "{{username}}" は既に存在します。既存のドメインを編集してください。',
connectionFailed: "接続に失敗しました",
unknownError: "不明なエラー",
rollbackNotImplemented: "ロールバック機能はまだ実装されていません",
@@ -101,11 +98,7 @@ function interpolate(message: string, params?: ErrorParams): string {
* @param params - Optional interpolation parameters
* @param locale - Optional locale override (defaults to stored preference)
*/
export function getErrorMessage(
key: MainErrorKey,
params?: ErrorParams,
locale?: LocaleCode,
): string {
export function getErrorMessage(key: MainErrorKey, params?: ErrorParams, locale?: LocaleCode): string {
const targetLocale = locale ?? getLocale();
const messages = errorMessages[targetLocale] || errorMessages["zh-CN"];
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 { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { registerIpcHandlers } from "./ipc-handlers";
import {
initializeStorage,
isSecureStorageAvailable,
getStorageBackend,
} from "./storage";
import { initializeStorage, isSecureStorageAvailable, getStorageBackend } from "./storage";
function createWindow(): void {
// Create the browser window.
@@ -71,9 +67,7 @@ app.whenReady().then(() => {
// Check secure storage availability
if (!isSecureStorageAvailable()) {
console.warn(
`Warning: Secure storage not available (backend: ${getStorageBackend()})`,
);
console.warn(`Warning: Secure storage not available (backend: ${getStorageBackend()})`);
console.warn("Passwords will not be securely encrypted on this system.");
}

View File

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

View File

@@ -1,13 +1,7 @@
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
import type { KintoneRestAPIError } from "@kintone/rest-api-client";
import type { DomainWithPassword } from "@shared/types/domain";
import {
type AppResponse,
type AppDetail,
type FileContent,
type KintoneApiError,
AppCustomizeParameter,
} from "@shared/types/kintone";
import { type AppResponse, type AppDetail, type FileContent, type KintoneApiError, AppCustomizeParameter } from "@shared/types/kintone";
import { getErrorMessage } from "./errors";
/**
@@ -18,11 +12,7 @@ export class KintoneError extends Error {
public readonly id?: string;
public readonly statusCode?: number;
constructor(
message: string,
apiError?: KintoneApiError,
statusCode?: number,
) {
constructor(message: string, apiError?: KintoneApiError, statusCode?: number) {
super(message);
this.name = "KintoneError";
this.code = apiError?.code;
@@ -53,11 +43,7 @@ export class KintoneClient {
private convertError(error: unknown): KintoneError {
if (error && typeof error === "object" && "code" in error) {
const apiError = error as KintoneRestAPIError;
return new KintoneError(
apiError.message,
{ code: apiError.code, message: apiError.message, id: apiError.id },
apiError.status,
);
return new KintoneError(apiError.message, { code: apiError.code, message: apiError.message, id: apiError.id }, apiError.status);
}
if (error instanceof Error) {
@@ -81,10 +67,7 @@ export class KintoneClient {
* Get all apps with pagination support
* Fetches all apps by making multiple requests if needed
*/
async getApps(options?: {
limit?: number;
offset?: number;
}): Promise<AppResponse[]> {
async getApps(options?: { limit?: number; offset?: number }): Promise<AppResponse[]> {
return this.withErrorHandling(async () => {
// If pagination options provided, use them directly
if (options?.limit !== undefined || options?.offset !== undefined) {
@@ -149,11 +132,7 @@ export class KintoneClient {
});
}
async uploadFile(
content: string | Buffer,
fileName: string,
_mimeType?: string,
): Promise<{ fileKey: string }> {
async uploadFile(content: string | Buffer, fileName: string, _mimeType?: string): Promise<{ fileKey: string }> {
return this.withErrorHandling(async () => {
const response = await this.client.file.uploadFile({
file: { name: fileName, data: content },
@@ -164,10 +143,7 @@ export class KintoneClient {
// ==================== Deploy APIs ====================
async updateAppCustomize(
appId: string,
config: Omit<AppCustomizeParameter, "app">,
): Promise<void> {
async updateAppCustomize(appId: string, config: Omit<AppCustomizeParameter, "app">): Promise<void> {
return this.withErrorHandling(async () => {
await this.client.app.updateAppCustomize({ ...config, app: appId });
});
@@ -179,9 +155,7 @@ export class KintoneClient {
});
}
async getDeployStatus(
appId: string,
): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
async getDeployStatus(appId: string): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
return this.withErrorHandling(async () => {
const response = await this.client.app.getDeployStatus({ apps: [appId] });
return response.apps[0]?.status || "FAIL";
@@ -198,10 +172,7 @@ export class KintoneClient {
} catch (error) {
return {
success: false,
error:
error instanceof KintoneError
? error.message
: getErrorMessage("connectionFailed"),
error: 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 path from "path";
import type { Domain, DomainWithPassword } from "@shared/types/domain";
import type {
Version,
DownloadMetadata,
BackupMetadata,
} from "@shared/types/version";
import type { Version, DownloadMetadata, BackupMetadata } from "@shared/types/version";
import type { LocaleCode } 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 {
try {
// Check if the method exists (added in Electron 30+)
if (typeof safeStorage.getSelectedStorageBackend === 'function') {
const backend = safeStorage.getSelectedStorageBackend()
return backend !== 'basic_text'
if (typeof safeStorage.getSelectedStorageBackend === "function") {
const backend = safeStorage.getSelectedStorageBackend();
return backend !== "basic_text";
}
// Fallback: check if encryption is available
return safeStorage.isEncryptionAvailable()
return safeStorage.isEncryptionAvailable();
} catch {
return false
return false;
}
}
@@ -123,12 +119,12 @@ export function isSecureStorageAvailable(): boolean {
*/
export function getStorageBackend(): string {
try {
if (typeof safeStorage.getSelectedStorageBackend === 'function') {
return safeStorage.getSelectedStorageBackend()
if (typeof safeStorage.getSelectedStorageBackend === "function") {
return safeStorage.getSelectedStorageBackend();
}
return 'unknown'
return "unknown";
} catch {
return 'unknown'
return "unknown";
}
}
@@ -139,9 +135,7 @@ export function encryptPassword(password: string): Buffer {
try {
return safeStorage.encryptString(password);
} catch (error) {
throw new Error(
`Failed to encrypt password: ${error instanceof Error ? error.message : "Unknown error"}`,
);
throw new Error(`Failed to encrypt password: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
@@ -152,9 +146,7 @@ export function decryptPassword(encrypted: Buffer): string {
try {
return safeStorage.decryptString(encrypted);
} catch (error) {
throw new Error(
`Failed to decrypt password: ${error instanceof Error ? error.message : "Unknown error"}`,
);
throw new 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
*/
export async function saveDomain(
domain: Domain,
password: string,
): Promise<void> {
export async function saveDomain(domain: Domain, password: string): Promise<void> {
const configPath = getConfigPath();
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
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
*/
export async function getDomain(
id: string,
): Promise<DomainWithPassword | null> {
export async function getDomain(id: string): Promise<DomainWithPassword | null> {
const configPath = getConfigPath();
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
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
*/
export async function saveVersion(version: Version): Promise<void> {
const versionDir = getStoragePath(
"versions",
version.domainId,
version.appId,
version.fileType,
version.id,
);
const versionDir = getStoragePath("versions", version.domainId, version.appId, version.fileType, version.id);
ensureDir(versionDir);
@@ -271,10 +252,7 @@ export async function saveVersion(version: Version): Promise<void> {
/**
* List versions for a specific app
*/
export async function listVersions(
domainId: string,
appId: string,
): Promise<Version[]> {
export async function listVersions(domainId: string, appId: string): Promise<Version[]> {
const versions: Version[] = [];
const baseDir = getStoragePath("versions", domainId, appId);
@@ -304,9 +282,7 @@ export async function listVersions(
}
// Sort by createdAt descending
return versions.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
return versions.sort((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
*/
export async function saveDownload(
metadata: DownloadMetadata,
files: Map<string, Buffer>,
): Promise<string> {
export async function saveDownload(metadata: DownloadMetadata, files: Map<string, Buffer>): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const downloadDir = getStoragePath(
"downloads",
metadata.domainId,
metadata.appId,
timestamp,
);
const downloadDir = getStoragePath("downloads", metadata.domainId, metadata.appId, timestamp);
ensureDir(downloadDir);
@@ -392,18 +360,9 @@ export function getDownloadPath(domainId: string, appId?: string): string {
/**
* Save backup files with metadata
*/
export async function saveBackup(
metadata: BackupMetadata,
files: Map<string, Buffer>,
): Promise<string> {
export async function saveBackup(metadata: BackupMetadata, files: Map<string, Buffer>): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupDir = getStoragePath(
"versions",
metadata.domainId,
metadata.appId,
"backup",
timestamp,
);
const backupDir = getStoragePath("versions", metadata.domainId, metadata.appId, "backup", timestamp);
ensureDir(backupDir);
@@ -437,12 +396,7 @@ export async function saveCustomizationFile(params: {
}): Promise<{ storagePath: string; fileName: string; size: number }> {
const { domainId, appId, platform, fileType, fileId, sourcePath } = params;
const fileName = path.basename(sourcePath);
const dir = getStoragePath(
"files",
domainId,
appId,
`${platform}_${fileType}`,
);
const dir = getStoragePath("files", domainId, appId, `${platform}_${fileType}`);
ensureDir(dir);
const storagePath = path.join(dir, `${fileId}_${fileName}`);
fs.copyFileSync(sourcePath, storagePath);
@@ -453,9 +407,7 @@ export async function saveCustomizationFile(params: {
/**
* Delete a customization file from storage.
*/
export async function deleteCustomizationFile(
storagePath: string,
): Promise<void> {
export async function deleteCustomizationFile(storagePath: string): Promise<void> {
if (fs.existsSync(storagePath)) {
fs.unlinkSync(storagePath);
}

View File

@@ -23,12 +23,7 @@ import type {
FileDeleteParams,
} from "@shared/types/ipc";
import type { Domain, DomainWithStatus } from "@shared/types/domain";
import type {
AppResponse,
AppDetail,
FileContent,
KintoneSpace,
} from "@shared/types/kintone";
import type { AppResponse, AppDetail, FileContent, KintoneSpace } from "@shared/types/kintone";
import type { Version } from "@shared/types/version";
import type { LocaleCode } from "@shared/types/locale";
@@ -49,16 +44,12 @@ export interface SelfAPI {
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
deleteDomain: (id: string) => Promise<Result<void>>;
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
testDomainConnection: (
params: TestDomainConnectionParams,
) => Promise<Result<boolean>>;
testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
// ==================== Browse ====================
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
getFileContent: (
params: GetFileContentParams,
) => Promise<Result<FileContent>>;
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
// ==================== Deploy ====================
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 {
Cloud,
History,
PanelLeftClose,
PanelLeftOpen,
Settings as SettingsIcon,
} from "lucide-react";
import { Cloud, History, PanelLeftClose, PanelLeftOpen, Settings as SettingsIcon } from "lucide-react";
import { createStyles, useTheme } from "antd-style";
import { useDomainStore } from "@renderer/stores";
@@ -158,20 +152,11 @@ const App: React.FC = () => {
}, []);
const { currentDomain } = useDomainStore();
const {
sidebarWidth,
siderCollapsed,
domainExpanded,
setSidebarWidth,
setSiderCollapsed,
setDomainExpanded,
} = useUIStore();
const { sidebarWidth, siderCollapsed, domainExpanded, setSidebarWidth, setSiderCollapsed, setDomainExpanded } = useUIStore();
const [settingsOpen, setSettingsOpen] = React.useState(false);
const [isResizing, setIsResizing] = React.useState(false);
const domainSectionHeight = domainExpanded
? DOMAIN_SECTION_EXPANDED
: DOMAIN_SECTION_COLLAPSED;
const domainSectionHeight = domainExpanded ? DOMAIN_SECTION_EXPANDED : DOMAIN_SECTION_COLLAPSED;
// Handle resize start
const handleResizeStart = React.useCallback(
@@ -184,10 +169,7 @@ const App: React.FC = () => {
const handleMouseMove = (moveEvent: MouseEvent) => {
const delta = moveEvent.clientX - startX;
const newWidth = Math.min(
MAX_SIDER_WIDTH,
Math.max(MIN_SIDER_WIDTH, startWidth + delta),
);
const newWidth = Math.min(MAX_SIDER_WIDTH, Math.max(MIN_SIDER_WIDTH, startWidth + delta));
setSidebarWidth(newWidth);
};
@@ -200,7 +182,7 @@ const App: React.FC = () => {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[sidebarWidth, setSidebarWidth],
[sidebarWidth, setSidebarWidth]
);
const toggleSider = () => {
@@ -223,29 +205,14 @@ const App: React.FC = () => {
<span className={styles.logoText}>Kintone JS/CSS Manager</span>
</div>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
<Button
type="text"
icon={<PanelLeftClose size={16} />}
onClick={toggleSider}
className={styles.siderCloseButton}
size="small"
/>
<Button type="text" icon={<PanelLeftClose size={16} />} onClick={toggleSider} className={styles.siderCloseButton} size="small" />
</Tooltip>
</div>
<div className={styles.siderContent}>
<div
className={styles.domainSection}
style={{ height: domainSectionHeight }}
>
<DomainManager
collapsed={!domainExpanded}
onToggleCollapse={() => setDomainExpanded(!domainExpanded)}
/>
<div className={styles.domainSection} style={{ height: domainSectionHeight }}>
<DomainManager collapsed={!domainExpanded} onToggleCollapse={() => setDomainExpanded(!domainExpanded)} />
</div>
<div
className={styles.appSection}
style={{ height: `calc(100% - ${domainSectionHeight}px)` }}
>
<div className={styles.appSection} style={{ height: `calc(100% - ${domainSectionHeight}px)` }}>
<AppList />
</div>
</div>
@@ -262,20 +229,12 @@ const App: React.FC = () => {
</Sider>
{/* Main Content */}
<Layout
className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`}
style={{ marginLeft: siderCollapsed ? 0 : sidebarWidth }}
>
<Layout className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`} style={{ marginLeft: siderCollapsed ? 0 : sidebarWidth }}>
<Header className={styles.header}>
<div className={styles.headerLeft}>
{siderCollapsed && (
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
<Button
type="text"
icon={<PanelLeftOpen size={16} />}
onClick={toggleSider}
size="small"
/>
<Button type="text" icon={<PanelLeftOpen size={16} />} onClick={toggleSider} size="small" />
</Tooltip>
)}
<Title level={5} style={{ margin: 0 }}>
@@ -287,10 +246,7 @@ const App: React.FC = () => {
{t("versionHistory")}
</Button>
<Tooltip title={t("settings")}>
<Button
icon={<SettingsIcon size={16} />}
onClick={() => setSettingsOpen(true)}
/>
<Button icon={<SettingsIcon size={16} />} onClick={() => setSettingsOpen(true)} />
</Tooltip>
</Space>
</Header>
@@ -305,14 +261,7 @@ const App: React.FC = () => {
</Layout>
{/* Settings Modal */}
<Modal
title={t("settings")}
open={settingsOpen}
onCancel={() => setSettingsOpen(false)}
footer={null}
width={480}
mask={{ closable: false }}
>
<Modal title={t("settings")} open={settingsOpen} onCancel={() => setSettingsOpen(false)} footer={null} width={480} mask={{ closable: false }}>
<Settings />
</Modal>
</Layout>

View File

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

View File

@@ -62,32 +62,16 @@ const DropZone: React.FC<DropZoneProps> = ({ fileType, isSaving, onFileSelected
}
e.target.value = "";
},
[onFileSelected],
[onFileSelected]
);
return (
<>
<button
className={styles.button}
onClick={handleClick}
disabled={isSaving}
type="button"
>
<button className={styles.button} onClick={handleClick} disabled={isSaving} type="button">
<CloudUpload size={14} />
<span>
{isSaving
? t("loading", { ns: "common" })
: t("dropZoneHint", { fileType: `.${fileType}` })}
</span>
<span>{isSaving ? t("loading", { ns: "common" }) : t("dropZoneHint", { fileType: `.${fileType}` })}</span>
</button>
<input
ref={inputRef}
type="file"
accept={`.${fileType}`}
multiple
style={{ display: "none" }}
onChange={handleChange}
/>
<input 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`;
};
const FileItem: React.FC<FileItemProps> = ({
entry,
onDelete,
onRestore,
onView,
onDownload,
isDownloading,
}) => {
const FileItem: React.FC<FileItemProps> = ({ entry, onDelete, onRestore, onView, onDownload, isDownloading }) => {
const { t } = useTranslation(["app", "common"]);
const { styles, cx } = useStyles();
const token = useTheme();
@@ -95,96 +88,38 @@ const FileItem: React.FC<FileItemProps> = ({
<div className={styles.item}>
<div className={styles.fileInfo}>
<SortableList.DragHandle />
{entry.status !== "unchanged" && (
<div
className={styles.statusDot}
style={{ background: statusColor[entry.status] }}
/>
)}
<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 !== "unchanged" && <div className={styles.statusDot} style={{ background: statusColor[entry.status] }} />}
<MaterialFileTypeIcon type="file" filename={`file.${entry.fileType}`} size={16} />
<span className={cx(styles.fileName, entry.status === "deleted" && styles.fileNameDeleted)}>{entry.fileName}</span>
{entry.status === "added" && <Badge color={token.colorSuccess} text={t("statusAdded")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />}
{entry.status === "deleted" && (
<Badge
color={token.colorError}
text={t("statusDeleted")}
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
/>
<Badge color={token.colorError} text={t("statusDeleted")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
)}
{entry.status === "reordered" && (
<Badge
color={token.colorWarning}
text={t("statusReordered")}
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
/>
<Badge color={token.colorWarning} text={t("statusReordered")} style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }} />
)}
</div>
<Space>
{entry.size && (
<span className={styles.fileSize}>{formatFileSize(entry.size)}</span>
)}
{entry.size && <span className={styles.fileSize}>{formatFileSize(entry.size)}</span>}
{entry.status === "deleted" ? (
<Button
type="text"
size="small"
icon={<Undo2 size={16} />}
onClick={onRestore}
>
<Button type="text" size="small" icon={<Undo2 size={16} />} onClick={onRestore}>
{t("restore")}
</Button>
) : (
<>
{(entry.status === "unchanged" || entry.status === "reordered") &&
onView &&
entry.fileKey && (
<Button
type="text"
size="small"
icon={<Code size={16} />}
onClick={onView}
>
{t("view")}
</Button>
)}
{(entry.status === "unchanged" || entry.status === "reordered") &&
onDownload &&
entry.fileKey && (
<Button
type="text"
size="small"
icon={<Download size={16} />}
loading={isDownloading}
onClick={onDownload}
>
{t("download", { ns: "common" })}
</Button>
)}
<Button
type="text"
size="small"
danger
icon={<Trash2 size={16} />}
onClick={onDelete}
/>
{(entry.status === "unchanged" || entry.status === "reordered") && onView && entry.fileKey && (
<Button type="text" size="small" icon={<Code size={16} />} onClick={onView}>
{t("view")}
</Button>
)}
{(entry.status === "unchanged" || entry.status === "reordered") && onDownload && entry.fileKey && (
<Button type="text" size="small" icon={<Download size={16} />} loading={isDownloading} onClick={onDownload}>
{t("download", { ns: "common" })}
</Button>
)}
<Button type="text" size="small" danger icon={<Trash2 size={16} />} onClick={onDelete} />
</>
)}
</Space>
@@ -192,4 +127,4 @@ const FileItem: React.FC<FileItemProps> = ({
);
};
export default FileItem;
export default FileItem;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,4 +60,4 @@
"updateFailed": "Update failed",
"expand": "Expand",
"collapse": "Collapse"
}
}

View File

@@ -60,4 +60,4 @@
"updateFailed": "更新に失敗しました",
"expand": "展開",
"collapse": "折りたたむ"
}
}

View File

@@ -60,4 +60,4 @@
"updateFailed": "更新失败",
"expand": "展开",
"collapse": "折叠"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,8 +45,7 @@ export const useVersionStore = create<VersionState>()((set) => ({
removeVersion: (id) =>
set((state) => ({
versions: state.versions.filter((v) => v.id !== id),
selectedVersion:
state.selectedVersion?.id === id ? null : state.selectedVersion,
selectedVersion: state.selectedVersion?.id === id ? null : state.selectedVersion,
})),
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";
// Unified result type
export type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
export type Result<T> = { success: true; data: T } | { success: false; error: string };
// ==================== Domain IPC Types ====================
@@ -193,16 +191,12 @@ export interface ElectronAPI {
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
deleteDomain: (id: string) => Promise<Result<void>>;
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
testDomainConnection: (
params: TestDomainConnectionParams,
) => Promise<Result<boolean>>;
testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
// Browse
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
getFileContent: (
params: GetFileContentParams,
) => Promise<Result<FileContent>>;
getFileContent: (params: GetFileContentParams) => Promise<Result<FileContent>>;
// Deploy
deploy: (params: DeployParams) => Promise<Result<DeployResult>>;

View File

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

View File

@@ -43,4 +43,4 @@ export const LOCALES: LocaleConfig[] = [
name: "English",
nativeName: "English",
},
];
];

View File

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

View File

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