chore: adjust printWidth to 160 and format code
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 200,
|
||||
"printWidth": 160,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
|
||||
8
src/main/env.d.ts
vendored
8
src/main/env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
15
src/preload/index.d.ts
vendored
15
src/preload/index.d.ts
vendored
@@ -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>>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
8
src/renderer/src/env.d.ts
vendored
8
src/renderer/src/env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,4 @@
|
||||
"updateFailed": "Update failed",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,4 @@
|
||||
"updateFailed": "更新に失敗しました",
|
||||
"expand": "展開",
|
||||
"collapse": "折りたたむ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,4 @@
|
||||
"updateFailed": "更新失败",
|
||||
"expand": "展开",
|
||||
"collapse": "折叠"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -100,6 +100,6 @@ export const useAppStore = create<AppState>()(
|
||||
loadedAt: state.loadedAt,
|
||||
selectedAppId: state.selectedAppId,
|
||||
}),
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -29,6 +29,6 @@ export const useLocaleStore = create<LocaleState>()(
|
||||
partialize: (state) => ({
|
||||
locale: state.locale,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -49,6 +49,6 @@ export const useSessionStore = create<SessionState>()(
|
||||
viewMode: state.viewMode,
|
||||
selectedFile: state.selectedFile,
|
||||
}),
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -27,6 +27,6 @@ export const useThemeStore = create<ThemeState>()(
|
||||
}),
|
||||
{
|
||||
name: "theme-storage",
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,4 @@ export const LOCALES: LocaleConfig[] = [
|
||||
name: "English",
|
||||
nativeName: "English",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user