update
This commit is contained in:
@@ -4,15 +4,28 @@ import react from '@vitejs/plugin-react'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@shared': resolve('src/shared'),
|
||||||
|
'@main': resolve('src/main')
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@shared': resolve('src/shared'),
|
||||||
|
'@preload': resolve('src/preload')
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src'),
|
||||||
|
'@shared': resolve('src/shared')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [react()]
|
plugins: [react()]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
||||||
import { SelfKintoneClient, createKintoneClient } from "@main/kintone-api";
|
import { KintoneClient, createKintoneClient } from "@main/kintone-api";
|
||||||
import type { DomainWithPassword } from "@shared/types/domain";
|
import type { DomainWithPassword } from "@shared/types/domain";
|
||||||
|
|
||||||
// Test configuration
|
// Test configuration
|
||||||
@@ -25,7 +25,7 @@ const TEST_CONFIG: DomainWithPassword = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("SelfKintoneClient - API Integration Tests", () => {
|
describe("SelfKintoneClient - API Integration Tests", () => {
|
||||||
let client: SelfKintoneClient;
|
let client: KintoneClient;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Create client with test credentials
|
// Create client with test credentials
|
||||||
@@ -137,7 +137,7 @@ describe("SelfKintoneClient - API Integration Tests", () => {
|
|||||||
describe("Create Kintone Client Factory", () => {
|
describe("Create Kintone Client Factory", () => {
|
||||||
it("should create a client instance", () => {
|
it("should create a client instance", () => {
|
||||||
const client = createKintoneClient(TEST_CONFIG);
|
const client = createKintoneClient(TEST_CONFIG);
|
||||||
expect(client).toBeInstanceOf(SelfKintoneClient);
|
expect(client).toBeInstanceOf(KintoneClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the correct domain", () => {
|
it("should return the correct domain", () => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
saveDownload,
|
saveDownload,
|
||||||
saveBackup,
|
saveBackup,
|
||||||
} from "./storage";
|
} from "./storage";
|
||||||
import { SelfKintoneClient, createKintoneClient } from "./kintone-api";
|
import { KintoneClient, createKintoneClient } from "./kintone-api";
|
||||||
import type { Result } from "@shared/types/ipc";
|
import type { Result } from "@shared/types/ipc";
|
||||||
import type {
|
import type {
|
||||||
CreateDomainParams,
|
CreateDomainParams,
|
||||||
@@ -32,21 +32,27 @@ import type {
|
|||||||
GetVersionsParams,
|
GetVersionsParams,
|
||||||
RollbackParams,
|
RollbackParams,
|
||||||
} from "@shared/types/ipc";
|
} from "@shared/types/ipc";
|
||||||
import type { Domain, DomainWithStatus, DomainWithPassword } from "@shared/types/domain";
|
import type {
|
||||||
|
Domain,
|
||||||
|
DomainWithStatus,
|
||||||
|
DomainWithPassword,
|
||||||
|
} from "@shared/types/domain";
|
||||||
import type {
|
import type {
|
||||||
Version,
|
Version,
|
||||||
DownloadMetadata,
|
DownloadMetadata,
|
||||||
BackupMetadata,
|
BackupMetadata,
|
||||||
|
DownloadFile,
|
||||||
} from "@shared/types/version";
|
} from "@shared/types/version";
|
||||||
import type { AppCustomizeResponse } from "@shared/types/kintone";
|
import type { AppCustomizeParameter, AppDetail } from "@shared/types/kintone";
|
||||||
|
import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
||||||
|
|
||||||
// Cache for Kintone clients
|
// Cache for Kintone clients
|
||||||
const clientCache = new Map<string, SelfKintoneClient>();
|
const clientCache = new Map<string, KintoneClient>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a Kintone client for a domain
|
* Get or create a Kintone client for a domain
|
||||||
*/
|
*/
|
||||||
async function getClient(domainId: string): Promise<SelfKintoneClient> {
|
async function getClient(domainId: string): Promise<KintoneClient> {
|
||||||
if (clientCache.has(domainId)) {
|
if (clientCache.has(domainId)) {
|
||||||
return clientCache.get(domainId)!;
|
return clientCache.get(domainId)!;
|
||||||
}
|
}
|
||||||
@@ -64,28 +70,15 @@ async function getClient(domainId: string): Promise<SelfKintoneClient> {
|
|||||||
/**
|
/**
|
||||||
* Helper to wrap IPC handlers with error handling
|
* Helper to wrap IPC handlers with error handling
|
||||||
*/
|
*/
|
||||||
function handle<T>(channel: string, handler: () => Promise<T>): void {
|
function handle<P = void, T = unknown>(
|
||||||
ipcMain.handle(channel, async (): Promise<Result<T>> => {
|
|
||||||
try {
|
|
||||||
const data = await handler();
|
|
||||||
return { success: true, data };
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
|
||||||
return { success: false, error: message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to wrap IPC handlers with parameters
|
|
||||||
*/
|
|
||||||
function handleWithParams<P, T>(
|
|
||||||
channel: string,
|
channel: string,
|
||||||
handler: (params: P) => Promise<T>,
|
handler: (params: P) => Promise<T>,
|
||||||
): void {
|
): void {
|
||||||
ipcMain.handle(channel, async (_event, params: P): Promise<Result<T>> => {
|
ipcMain.handle(channel, async (_event, params: P): Promise<Result<T>> => {
|
||||||
try {
|
try {
|
||||||
const data = await handler(params);
|
// For handlers without params (P=void), params will be undefined but handler ignores it
|
||||||
|
// For handlers with params, params will be the typed value
|
||||||
|
const data = await handler(params as P);
|
||||||
return { success: true, data };
|
return { success: true, data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -110,20 +103,18 @@ function registerGetDomains(): void {
|
|||||||
* Deduplication: Check if domain+username already exists
|
* Deduplication: Check if domain+username already exists
|
||||||
*/
|
*/
|
||||||
function registerCreateDomain(): void {
|
function registerCreateDomain(): void {
|
||||||
handleWithParams<CreateDomainParams, Domain>(
|
handle<CreateDomainParams, Domain>("createDomain", async (params) => {
|
||||||
"createDomain",
|
|
||||||
async (params) => {
|
|
||||||
// Check for duplicate domain+username
|
// Check for duplicate domain+username
|
||||||
const existingDomains = await listDomains();
|
const existingDomains = await listDomains();
|
||||||
const duplicate = existingDomains.find(
|
const duplicate = existingDomains.find(
|
||||||
(d) =>
|
(d) =>
|
||||||
d.domain.toLowerCase() === params.domain.toLowerCase() &&
|
d.domain.toLowerCase() === params.domain.toLowerCase() &&
|
||||||
d.username.toLowerCase() === params.username.toLowerCase()
|
d.username.toLowerCase() === params.username.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Domain "${params.domain}" with user "${params.username}" already exists. Please edit the existing domain instead.`
|
`Domain "${params.domain}" with user "${params.username}" already exists. Please edit the existing domain instead.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,17 +132,14 @@ function registerCreateDomain(): void {
|
|||||||
|
|
||||||
await saveDomain(domain, params.password);
|
await saveDomain(domain, params.password);
|
||||||
return domain;
|
return domain;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing domain
|
* Update an existing domain
|
||||||
*/
|
*/
|
||||||
function registerUpdateDomain(): void {
|
function registerUpdateDomain(): void {
|
||||||
handleWithParams<UpdateDomainParams, Domain>(
|
handle<UpdateDomainParams, Domain>("updateDomain", async (params) => {
|
||||||
"updateDomain",
|
|
||||||
async (params) => {
|
|
||||||
const domains = await listDomains();
|
const domains = await listDomains();
|
||||||
const existing = domains.find((d) => d.id === params.id);
|
const existing = domains.find((d) => d.id === params.id);
|
||||||
|
|
||||||
@@ -184,15 +172,14 @@ function registerUpdateDomain(): void {
|
|||||||
clientCache.delete(params.id);
|
clientCache.delete(params.id);
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a domain
|
* Delete a domain
|
||||||
*/
|
*/
|
||||||
function registerDeleteDomain(): void {
|
function registerDeleteDomain(): void {
|
||||||
handleWithParams<string, void>("deleteDomain", async (id) => {
|
handle<string, void>("deleteDomain", async (id) => {
|
||||||
await deleteDomain(id);
|
await deleteDomain(id);
|
||||||
clientCache.delete(id);
|
clientCache.delete(id);
|
||||||
});
|
});
|
||||||
@@ -202,7 +189,7 @@ function registerDeleteDomain(): void {
|
|||||||
* Test domain connection
|
* Test domain connection
|
||||||
*/
|
*/
|
||||||
function registerTestConnection(): void {
|
function registerTestConnection(): void {
|
||||||
handleWithParams<string, DomainWithStatus>("testConnection", async (id) => {
|
handle<string, DomainWithStatus>("testConnection", async (id) => {
|
||||||
const domainWithPassword = await getDomain(id);
|
const domainWithPassword = await getDomain(id);
|
||||||
if (!domainWithPassword) {
|
if (!domainWithPassword) {
|
||||||
throw new Error(`Domain not found: ${id}`);
|
throw new Error(`Domain not found: ${id}`);
|
||||||
@@ -223,7 +210,7 @@ function registerTestConnection(): void {
|
|||||||
* Test domain connection with temporary credentials
|
* Test domain connection with temporary credentials
|
||||||
*/
|
*/
|
||||||
function registerTestDomainConnection(): void {
|
function registerTestDomainConnection(): void {
|
||||||
handleWithParams<TestDomainConnectionParams, boolean>(
|
handle<TestDomainConnectionParams, boolean>(
|
||||||
"testDomainConnection",
|
"testDomainConnection",
|
||||||
async (params) => {
|
async (params) => {
|
||||||
const tempDomain: DomainWithPassword = {
|
const tempDomain: DomainWithPassword = {
|
||||||
@@ -256,27 +243,25 @@ function registerTestDomainConnection(): void {
|
|||||||
* Get apps
|
* Get apps
|
||||||
*/
|
*/
|
||||||
function registerGetApps(): void {
|
function registerGetApps(): void {
|
||||||
handleWithParams<
|
handle<GetAppsParams, Awaited<ReturnType<KintoneClient["getApps"]>>>(
|
||||||
GetAppsParams,
|
"getApps",
|
||||||
Awaited<ReturnType<SelfKintoneClient["getApps"]>>
|
async (params) => {
|
||||||
>("getApps", async (params) => {
|
|
||||||
const client = await getClient(params.domainId);
|
const client = await getClient(params.domainId);
|
||||||
return client.getApps({
|
return client.getApps({
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
offset: params.offset,
|
offset: params.offset,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app detail
|
* Get app detail
|
||||||
*/
|
*/
|
||||||
function registerGetAppDetail(): void {
|
function registerGetAppDetail(): void {
|
||||||
handleWithParams<
|
handle<
|
||||||
GetAppDetailParams,
|
GetAppDetailParams,
|
||||||
Awaited<ReturnType<SelfKintoneClient["getAppDetail"]>>
|
Awaited<ReturnType<KintoneClient["getAppDetail"]>>
|
||||||
>("getAppDetail", async (params) => {
|
>("getAppDetail", async (params) => {
|
||||||
const client = await getClient(params.domainId);
|
const client = await getClient(params.domainId);
|
||||||
return client.getAppDetail(params.appId);
|
return client.getAppDetail(params.appId);
|
||||||
@@ -287,9 +272,9 @@ function registerGetAppDetail(): void {
|
|||||||
* Get file content
|
* Get file content
|
||||||
*/
|
*/
|
||||||
function registerGetFileContent(): void {
|
function registerGetFileContent(): void {
|
||||||
handleWithParams<
|
handle<
|
||||||
GetFileContentParams,
|
GetFileContentParams,
|
||||||
Awaited<ReturnType<SelfKintoneClient["getFileContent"]>>
|
Awaited<ReturnType<KintoneClient["getFileContent"]>>
|
||||||
>("getFileContent", async (params) => {
|
>("getFileContent", async (params) => {
|
||||||
const client = await getClient(params.domainId);
|
const client = await getClient(params.domainId);
|
||||||
return client.getFileContent(params.fileKey);
|
return client.getFileContent(params.fileKey);
|
||||||
@@ -298,11 +283,45 @@ function registerGetFileContent(): void {
|
|||||||
|
|
||||||
// ==================== Deploy IPC Handlers ====================
|
// ==================== Deploy IPC Handlers ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add files to backup for a specific platform and file type
|
||||||
|
*/
|
||||||
|
async function addFilesToBackup(
|
||||||
|
platform: "desktop" | "mobile",
|
||||||
|
fileType: "js" | "css",
|
||||||
|
client: KintoneClient,
|
||||||
|
appDetail: AppDetail,
|
||||||
|
backupFiles: Map<string, Buffer>,
|
||||||
|
backupFileList: DownloadFile[]
|
||||||
|
): Promise<void> {
|
||||||
|
const files = appDetail.customization?.[platform]?.[fileType] || [];
|
||||||
|
|
||||||
|
for (const [index, file] of files.entries()) {
|
||||||
|
const fileKey = getFileKey(file);
|
||||||
|
if (fileKey) {
|
||||||
|
const fileContent = await client.getFileContent(fileKey);
|
||||||
|
const content = Buffer.from(fileContent.content || "", "base64");
|
||||||
|
const fileName = getDisplayName(file, fileType, index);
|
||||||
|
const type = platform === "desktop" ? "pc" : "mobile";
|
||||||
|
|
||||||
|
backupFiles.set(`${type}/${fileName}`, content);
|
||||||
|
backupFileList.push({
|
||||||
|
type,
|
||||||
|
fileType,
|
||||||
|
fileName,
|
||||||
|
fileKey,
|
||||||
|
size: content.length,
|
||||||
|
path: `${type}/${fileName}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deploy files to Kintone
|
* Deploy files to Kintone
|
||||||
*/
|
*/
|
||||||
function registerDeploy(): void {
|
function registerDeploy(): void {
|
||||||
handleWithParams<DeployParams, DeployResult>("deploy", async (params) => {
|
handle<DeployParams, DeployResult>("deploy", async (params) => {
|
||||||
const client = await getClient(params.domainId);
|
const client = await getClient(params.domainId);
|
||||||
const domainWithPassword = await getDomain(params.domainId);
|
const domainWithPassword = await getDomain(params.domainId);
|
||||||
|
|
||||||
@@ -317,39 +336,14 @@ function registerDeploy(): void {
|
|||||||
const backupFiles = new Map<string, Buffer>();
|
const backupFiles = new Map<string, Buffer>();
|
||||||
const backupFileList: BackupMetadata["files"] = [];
|
const backupFileList: BackupMetadata["files"] = [];
|
||||||
|
|
||||||
// Add JS files to backup
|
// Add desktop files to backup
|
||||||
for (const js of appDetail.customization?.desktop?.js || []) {
|
await addFilesToBackup("desktop", "js", client, appDetail, backupFiles, backupFileList);
|
||||||
if (js.file?.fileKey) {
|
await addFilesToBackup("desktop", "css", client, appDetail, backupFiles, backupFileList);
|
||||||
const fileContent = await client.getFileContent(js.file.fileKey);
|
|
||||||
const content = Buffer.from(fileContent.content || "", "base64");
|
// Add mobile files to backup
|
||||||
backupFiles.set(`pc/${js.file.name || js.file.fileKey}.js`, content);
|
await addFilesToBackup("mobile", "js", client, appDetail, backupFiles, backupFileList);
|
||||||
backupFileList.push({
|
await addFilesToBackup("mobile", "css", client, appDetail, backupFiles, backupFileList);
|
||||||
type: "pc",
|
|
||||||
fileType: "js",
|
|
||||||
fileName: js.file.name || js.file.fileKey,
|
|
||||||
fileKey: js.file.fileKey,
|
|
||||||
size: content.length,
|
|
||||||
path: `pc/${js.file.name || js.file.fileKey}.js`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CSS files to backup
|
|
||||||
for (const css of appDetail.customization?.desktop?.css || []) {
|
|
||||||
if (css.file?.fileKey) {
|
|
||||||
const fileContent = await client.getFileContent(css.file.fileKey);
|
|
||||||
const content = Buffer.from(fileContent.content || "", "base64");
|
|
||||||
backupFiles.set(`pc/${css.file.name || css.file.fileKey}.css`, content);
|
|
||||||
backupFileList.push({
|
|
||||||
type: "pc",
|
|
||||||
fileType: "css",
|
|
||||||
fileName: css.file.name || css.file.fileKey,
|
|
||||||
fileKey: css.file.fileKey,
|
|
||||||
size: content.length,
|
|
||||||
path: `pc/${css.file.name || css.file.fileKey}.css`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Save backup
|
// Save backup
|
||||||
const backupMetadata: BackupMetadata = {
|
const backupMetadata: BackupMetadata = {
|
||||||
backedUpAt: new Date().toISOString(),
|
backedUpAt: new Date().toISOString(),
|
||||||
@@ -363,59 +357,42 @@ function registerDeploy(): void {
|
|||||||
|
|
||||||
const backupPath = await saveBackup(backupMetadata, backupFiles);
|
const backupPath = await saveBackup(backupMetadata, backupFiles);
|
||||||
|
|
||||||
// Upload new files
|
// Upload new files and build customization config directly
|
||||||
const uploadedFiles: Array<{
|
const newConfig: AppCustomizeParameter = {
|
||||||
type: "js" | "css";
|
app: params.appId,
|
||||||
position: string;
|
scope: "ALL",
|
||||||
fileKey: string;
|
desktop: {
|
||||||
}> = [];
|
js: [],
|
||||||
|
css: [],
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
js: [],
|
||||||
|
css: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
for (const file of params.files) {
|
for (const file of params.files) {
|
||||||
const fileKey = await client.uploadFile(file.content, file.fileName);
|
const fileKey = await client.uploadFile(file.content, file.fileName);
|
||||||
|
const fileEntry = { type: "FILE" as const, file: { fileKey: fileKey.fileKey } };
|
||||||
|
|
||||||
uploadedFiles.push({
|
// Add to corresponding field based on file type and position
|
||||||
type: file.fileType,
|
if (file.fileType === "js") {
|
||||||
position: file.position,
|
if (file.position.startsWith("pc_")) {
|
||||||
fileKey: fileKey.fileKey,
|
newConfig.desktop!.js!.push(fileEntry);
|
||||||
});
|
} else if (file.position.startsWith("mobile_")) {
|
||||||
|
newConfig.mobile!.js!.push(fileEntry);
|
||||||
|
}
|
||||||
|
} else if (file.fileType === "css") {
|
||||||
|
if (file.position === "pc_css") {
|
||||||
|
newConfig.desktop!.css!.push(fileEntry);
|
||||||
|
} else if (file.position === "mobile_css") {
|
||||||
|
newConfig.mobile!.css!.push(fileEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build new customization config
|
|
||||||
// Note: This is simplified - real implementation would merge with existing config
|
|
||||||
const newConfig: AppCustomizeResponse = {
|
|
||||||
desktop: {
|
|
||||||
js: uploadedFiles
|
|
||||||
.filter((f) => f.type === "js" && f.position.startsWith("pc_"))
|
|
||||||
.map((f) => ({
|
|
||||||
type: "FILE" as const,
|
|
||||||
file: { fileKey: f.fileKey },
|
|
||||||
})),
|
|
||||||
css: uploadedFiles
|
|
||||||
.filter((f) => f.type === "css" && f.position === "pc_css")
|
|
||||||
.map((f) => ({
|
|
||||||
type: "FILE" as const,
|
|
||||||
file: { fileKey: f.fileKey },
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
mobile: {
|
|
||||||
js: uploadedFiles
|
|
||||||
.filter((f) => f.type === "js" && f.position.startsWith("mobile_"))
|
|
||||||
.map((f) => ({
|
|
||||||
type: "FILE" as const,
|
|
||||||
file: { fileKey: f.fileKey },
|
|
||||||
})),
|
|
||||||
css: uploadedFiles
|
|
||||||
.filter((f) => f.type === "css" && f.position === "mobile_css")
|
|
||||||
.map((f) => ({
|
|
||||||
type: "FILE" as const,
|
|
||||||
file: { fileKey: f.fileKey },
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
scope: "ALL",
|
|
||||||
revision: "-1",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update app customization
|
// Update app customization
|
||||||
|
await client.updateAppCustomize(params.appId, newConfig);
|
||||||
|
|
||||||
// Deploy the changes
|
// Deploy the changes
|
||||||
await client.deployApp(params.appId);
|
await client.deployApp(params.appId);
|
||||||
@@ -434,9 +411,7 @@ function registerDeploy(): void {
|
|||||||
* Download files from Kintone
|
* Download files from Kintone
|
||||||
*/
|
*/
|
||||||
function registerDownload(): void {
|
function registerDownload(): void {
|
||||||
handleWithParams<DownloadParams, DownloadResult>(
|
handle<DownloadParams, DownloadResult>("download", async (params) => {
|
||||||
"download",
|
|
||||||
async (params) => {
|
|
||||||
const client = await getClient(params.domainId);
|
const client = await getClient(params.domainId);
|
||||||
const domainWithPassword = await getDomain(params.domainId);
|
const domainWithPassword = await getDomain(params.domainId);
|
||||||
|
|
||||||
@@ -466,20 +441,21 @@ function registerDownload(): void {
|
|||||||
? appDetail.customization?.mobile?.js
|
? appDetail.customization?.mobile?.js
|
||||||
: appDetail.customization?.mobile?.css;
|
: appDetail.customization?.mobile?.css;
|
||||||
|
|
||||||
for (const file of files || []) {
|
for (const [index, file] of (files || []).entries()) {
|
||||||
if (file.file?.fileKey) {
|
const fileKey = getFileKey(file);
|
||||||
const content = await client.getFileContent(file.file.fileKey);
|
if (fileKey) {
|
||||||
|
const content = await client.getFileContent(fileKey);
|
||||||
const buffer = Buffer.from(content.content || "", "base64");
|
const buffer = Buffer.from(content.content || "", "base64");
|
||||||
const fileName = file.file.name || file.file.fileKey;
|
|
||||||
const type = fileType.includes("mobile") ? "mobile" : "pc";
|
|
||||||
const ext = fileType.includes("js") ? "js" : "css";
|
const ext = fileType.includes("js") ? "js" : "css";
|
||||||
|
const fileName = getDisplayName(file, ext, index);
|
||||||
|
const type = fileType.includes("mobile") ? "mobile" : "pc";
|
||||||
|
|
||||||
downloadFiles.set(`${type}/${fileName}`, buffer);
|
downloadFiles.set(`${type}/${fileName}`, buffer);
|
||||||
downloadFileList.push({
|
downloadFileList.push({
|
||||||
type,
|
type,
|
||||||
fileType: ext,
|
fileType: ext,
|
||||||
fileName,
|
fileName,
|
||||||
fileKey: file.file.fileKey,
|
fileKey,
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
path: `${type}/${fileName}`,
|
path: `${type}/${fileName}`,
|
||||||
});
|
});
|
||||||
@@ -507,8 +483,7 @@ function registerDownload(): void {
|
|||||||
path,
|
path,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Version IPC Handlers ====================
|
// ==================== Version IPC Handlers ====================
|
||||||
@@ -517,19 +492,16 @@ function registerDownload(): void {
|
|||||||
* Get versions for an app
|
* Get versions for an app
|
||||||
*/
|
*/
|
||||||
function registerGetVersions(): void {
|
function registerGetVersions(): void {
|
||||||
handleWithParams<GetVersionsParams, Version[]>(
|
handle<GetVersionsParams, Version[]>("getVersions", async (params) => {
|
||||||
"getVersions",
|
|
||||||
async (params) => {
|
|
||||||
return listVersions(params.domainId, params.appId);
|
return listVersions(params.domainId, params.appId);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a version
|
* Delete a version
|
||||||
*/
|
*/
|
||||||
function registerDeleteVersion(): void {
|
function registerDeleteVersion(): void {
|
||||||
handleWithParams<string, void>("deleteVersion", async (id) => {
|
handle<string, void>("deleteVersion", async (id) => {
|
||||||
return deleteVersion(id);
|
return deleteVersion(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -538,7 +510,7 @@ function registerDeleteVersion(): void {
|
|||||||
* Rollback to a previous version
|
* Rollback to a previous version
|
||||||
*/
|
*/
|
||||||
function registerRollback(): void {
|
function registerRollback(): void {
|
||||||
handleWithParams<RollbackParams, DeployResult>("rollback", async (_params) => {
|
handle<RollbackParams, DeployResult>("rollback", async (_params) => {
|
||||||
// This would read the version file and redeploy
|
// This would read the version file and redeploy
|
||||||
// Simplified implementation - would need full implementation
|
// Simplified implementation - would need full implementation
|
||||||
throw new Error("Rollback not yet implemented");
|
throw new Error("Rollback not yet implemented");
|
||||||
@@ -577,3 +549,4 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
console.log("IPC handlers registered");
|
console.log("IPC handlers registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
import type { KintoneRestAPIError } from "@kintone/rest-api-client";
|
import type { KintoneRestAPIError } from '@kintone/rest-api-client';
|
||||||
import type { DomainWithPassword } from "@shared/types/domain";
|
import type { DomainWithPassword } from '@shared/types/domain';
|
||||||
import type {
|
import {
|
||||||
AppResponse,
|
type AppResponse,
|
||||||
AppCustomizeResponse,
|
type AppDetail,
|
||||||
AppDetail,
|
type FileContent,
|
||||||
FileContent,
|
type KintoneApiError,
|
||||||
KintoneApiError,
|
AppCustomizeParameter,
|
||||||
FileConfig,
|
} from '@shared/types/kintone';
|
||||||
} from "@shared/types/kintone";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom error class for Kintone API errors
|
* Custom error class for Kintone API errors
|
||||||
@@ -18,13 +17,9 @@ export class KintoneError extends Error {
|
|||||||
public readonly id?: string;
|
public readonly id?: string;
|
||||||
public readonly statusCode?: number;
|
public readonly statusCode?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(message: string, apiError?: KintoneApiError, statusCode?: number) {
|
||||||
message: string,
|
|
||||||
apiError?: KintoneApiError,
|
|
||||||
statusCode?: number,
|
|
||||||
) {
|
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "KintoneError";
|
this.name = 'KintoneError';
|
||||||
this.code = apiError?.code;
|
this.code = apiError?.code;
|
||||||
this.id = apiError?.id;
|
this.id = apiError?.id;
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
@@ -34,7 +29,7 @@ export class KintoneError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Kintone REST API Client
|
* Kintone REST API Client
|
||||||
*/
|
*/
|
||||||
export class SelfKintoneClient {
|
export class KintoneClient {
|
||||||
private client: KintoneRestAPIClient;
|
private client: KintoneRestAPIClient;
|
||||||
private domain: string;
|
private domain: string;
|
||||||
|
|
||||||
@@ -42,8 +37,8 @@ export class SelfKintoneClient {
|
|||||||
this.domain = domainConfig.domain;
|
this.domain = domainConfig.domain;
|
||||||
|
|
||||||
const auth =
|
const auth =
|
||||||
domainConfig.authType === "api_token"
|
domainConfig.authType === 'api_token'
|
||||||
? { apiToken: domainConfig.apiToken || "" }
|
? { apiToken: domainConfig.apiToken || '' }
|
||||||
: {
|
: {
|
||||||
username: domainConfig.username,
|
username: domainConfig.username,
|
||||||
password: domainConfig.password,
|
password: domainConfig.password,
|
||||||
@@ -56,7 +51,7 @@ export class SelfKintoneClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private convertError(error: unknown): KintoneError {
|
private convertError(error: unknown): KintoneError {
|
||||||
if (error && typeof error === "object" && "code" in error) {
|
if (error && typeof error === 'object' && 'code' in error) {
|
||||||
const apiError = error as KintoneRestAPIError;
|
const apiError = error as KintoneRestAPIError;
|
||||||
return new KintoneError(
|
return new KintoneError(
|
||||||
apiError.message,
|
apiError.message,
|
||||||
@@ -69,7 +64,7 @@ export class SelfKintoneClient {
|
|||||||
return new KintoneError(error.message);
|
return new KintoneError(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new KintoneError("Unknown error occurred");
|
return new KintoneError('Unknown error occurred');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async withErrorHandling<T>(operation: () => Promise<T>): Promise<T> {
|
private async withErrorHandling<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
@@ -80,31 +75,13 @@ export class SelfKintoneClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCustomizeSection(
|
|
||||||
files?: FileConfig[],
|
|
||||||
): NonNullable<
|
|
||||||
Parameters<KintoneRestAPIClient["app"]["updateAppCustomize"]>[0]["desktop"]
|
|
||||||
>["js"] {
|
|
||||||
if (!files || files.length === 0) return undefined;
|
|
||||||
|
|
||||||
return files.map((item) => {
|
|
||||||
if (item.type === "FILE" && item.file) {
|
|
||||||
return { type: "FILE" as const, file: { fileKey: item.file.fileKey } };
|
|
||||||
}
|
|
||||||
return { type: "URL" as const, url: item.url || "" };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== App APIs ====================
|
// ==================== App APIs ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all apps with pagination support
|
* Get all apps with pagination support
|
||||||
* Fetches all apps by making multiple requests if needed
|
* Fetches all apps by making multiple requests if needed
|
||||||
*/
|
*/
|
||||||
async getApps(options?: {
|
async getApps(options?: { limit?: number; offset?: number }): Promise<AppResponse[]> {
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
}): Promise<AppResponse[]> {
|
|
||||||
return this.withErrorHandling(async () => {
|
return this.withErrorHandling(async () => {
|
||||||
// If pagination options provided, use them directly
|
// If pagination options provided, use them directly
|
||||||
if (options?.limit !== undefined || options?.offset !== undefined) {
|
if (options?.limit !== undefined || options?.offset !== undefined) {
|
||||||
@@ -157,23 +134,19 @@ export class SelfKintoneClient {
|
|||||||
return this.withErrorHandling(async () => {
|
return this.withErrorHandling(async () => {
|
||||||
const data = await this.client.file.downloadFile({ fileKey });
|
const data = await this.client.file.downloadFile({ fileKey });
|
||||||
const buffer = Buffer.from(data);
|
const buffer = Buffer.from(data);
|
||||||
const content = buffer.toString("base64");
|
const content = buffer.toString('base64');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileKey,
|
fileKey,
|
||||||
name: fileKey,
|
name: fileKey,
|
||||||
size: buffer.byteLength,
|
size: buffer.byteLength,
|
||||||
mimeType: "application/octet-stream",
|
mimeType: 'application/octet-stream',
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(
|
async uploadFile(content: string | Buffer, fileName: string, _mimeType?: string): Promise<{ fileKey: string }> {
|
||||||
content: string | Buffer,
|
|
||||||
fileName: string,
|
|
||||||
_mimeType?: string,
|
|
||||||
): Promise<{ fileKey: string }> {
|
|
||||||
return this.withErrorHandling(async () => {
|
return this.withErrorHandling(async () => {
|
||||||
const response = await this.client.file.uploadFile({
|
const response = await this.client.file.uploadFile({
|
||||||
file: { name: fileName, data: content },
|
file: { name: fileName, data: content },
|
||||||
@@ -184,23 +157,9 @@ export class SelfKintoneClient {
|
|||||||
|
|
||||||
// ==================== Deploy APIs ====================
|
// ==================== Deploy APIs ====================
|
||||||
|
|
||||||
async updateAppCustomize(
|
async updateAppCustomize(appId: string, config: Omit<AppCustomizeParameter, 'app'>): Promise<void> {
|
||||||
appId: string,
|
|
||||||
config: AppCustomizeResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
return this.withErrorHandling(async () => {
|
return this.withErrorHandling(async () => {
|
||||||
await this.client.app.updateAppCustomize({
|
await this.client.app.updateAppCustomize({ ...config, app: appId });
|
||||||
app: appId,
|
|
||||||
desktop: {
|
|
||||||
js: this.buildCustomizeSection(config.desktop?.js),
|
|
||||||
css: this.buildCustomizeSection(config.desktop?.css),
|
|
||||||
},
|
|
||||||
mobile: {
|
|
||||||
js: this.buildCustomizeSection(config.mobile?.js),
|
|
||||||
css: this.buildCustomizeSection(config.mobile?.css),
|
|
||||||
},
|
|
||||||
scope: config.scope || "ALL",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,12 +169,10 @@ export class SelfKintoneClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployStatus(
|
async getDeployStatus(appId: string): Promise<'PROCESSING' | 'SUCCESS' | 'FAIL' | 'CANCEL'> {
|
||||||
appId: string,
|
|
||||||
): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
|
|
||||||
return this.withErrorHandling(async () => {
|
return this.withErrorHandling(async () => {
|
||||||
const response = await this.client.app.getDeployStatus({ apps: [appId] });
|
const response = await this.client.app.getDeployStatus({ apps: [appId] });
|
||||||
return response.apps[0]?.status || "FAIL";
|
return response.apps[0]?.status || 'FAIL';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +186,7 @@ export class SelfKintoneClient {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: error instanceof KintoneError ? error.message : 'Connection failed',
|
||||||
error instanceof KintoneError ? error.message : "Connection failed",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,8 +196,6 @@ export class SelfKintoneClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createKintoneClient(
|
export function createKintoneClient(domain: DomainWithPassword): KintoneClient {
|
||||||
domain: DomainWithPassword,
|
return new KintoneClient(domain);
|
||||||
): SelfKintoneClient {
|
|
||||||
return new SelfKintoneClient(domain);
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -45,7 +45,6 @@ export interface SelfAPI {
|
|||||||
) => Promise<Result<boolean>>;
|
) => Promise<Result<boolean>>;
|
||||||
|
|
||||||
// ==================== Browse ====================
|
// ==================== Browse ====================
|
||||||
getSpaces: (params: { domainId: string }) => Promise<Result<KintoneSpace[]>>;
|
|
||||||
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
||||||
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
||||||
getFileContent: (
|
getFileContent: (
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import { createStyles } from "antd-style";
|
|||||||
import { useAppStore } from "@renderer/stores";
|
import { useAppStore } from "@renderer/stores";
|
||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import { CodeViewer } from "../CodeViewer";
|
import { CodeViewer } from "../CodeViewer";
|
||||||
import { FileConfig } from "@shared/types/kintone";
|
import { FileConfigResponse } from "@shared/types/kintone";
|
||||||
|
import { getDisplayName, getFileKey, isFileUpload } from "@shared/utils/fileDisplay";
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
container: css`
|
container: css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -129,7 +129,7 @@ const AppDetail: React.FC = () => {
|
|||||||
const [activeTab, setActiveTab] = React.useState("info");
|
const [activeTab, setActiveTab] = React.useState("info");
|
||||||
const [selectedFile, setSelectedFile] = React.useState<{
|
const [selectedFile, setSelectedFile] = React.useState<{
|
||||||
type: "js" | "css";
|
type: "js" | "css";
|
||||||
fileKey: string;
|
fileKey?: string;
|
||||||
name: string;
|
name: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ const AppDetail: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderFileList = (
|
const renderFileList = (
|
||||||
files: (FileConfig)[] | undefined,
|
files: (FileConfigResponse)[] | undefined,
|
||||||
type: "js" | "css",
|
type: "js" | "css",
|
||||||
) => {
|
) => {
|
||||||
if (!files || files.length === 0) {
|
if (!files || files.length === 0) {
|
||||||
@@ -178,8 +178,9 @@ const AppDetail: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{files.map((file, index) => {
|
{files.map((file, index) => {
|
||||||
const fileName = file.file?.name || file.url || `文件 ${index + 1}`;
|
const fileName = getDisplayName(file, type, index);
|
||||||
const fileKey = file.file?.fileKey;
|
const fileKey = getFileKey(file);
|
||||||
|
const canView = isFileUpload(file);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -202,7 +203,7 @@ const AppDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{fileKey && (
|
{canView && (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -313,7 +314,7 @@ const AppDetail: React.FC = () => {
|
|||||||
{
|
{
|
||||||
key: "code",
|
key: "code",
|
||||||
label: "代码查看",
|
label: "代码查看",
|
||||||
children: selectedFile ? (
|
children: selectedFile && selectedFile.fileKey ? (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
fileKey={selectedFile.fileKey}
|
fileKey={selectedFile.fileKey}
|
||||||
fileName={selectedFile.name}
|
fileName={selectedFile.name}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import { useAppStore } from "@renderer/stores";
|
import { useAppStore } from "@renderer/stores";
|
||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import type { KintoneApp } from "@shared/types/kintone";
|
import type { AppDetail } from "@shared/types/kintone";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -160,12 +160,12 @@ const AppList: React.FC = () => {
|
|||||||
}, [apps, searchText]);
|
}, [apps, searchText]);
|
||||||
|
|
||||||
// Handle item click
|
// Handle item click
|
||||||
const handleItemClick = (app: KintoneApp) => {
|
const handleItemClick = (app: AppDetail) => {
|
||||||
setSelectedAppId(app.appId);
|
setSelectedAppId(app.appId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render list item
|
// Render list item
|
||||||
const renderItem = (app: KintoneApp) => {
|
const renderItem = (app: AppDetail) => {
|
||||||
const isActive = selectedAppId === app.appId;
|
const isActive = selectedAppId === app.appId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
export interface Domain {
|
export interface Domain {
|
||||||
id: string; // UUID
|
id: string; // UUID
|
||||||
name: string; // 自定义名称
|
name: string; // 自定义名称
|
||||||
domain: string; // Kintone 域名
|
|
||||||
username: string; // 用户名(邮箱)
|
username: string; // 用户名(邮箱)
|
||||||
authType: "password" | "api_token";
|
domain: string; // Kintone 域名
|
||||||
apiToken?: string; // 可选,当 authType 为 api_token 时
|
|
||||||
createdAt: string; // ISO 8601
|
createdAt: string; // ISO 8601
|
||||||
updatedAt: string; // ISO 8601
|
updatedAt: string; // ISO 8601
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
* Unified request/response format for all IPC handlers
|
* Unified request/response format for all IPC handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Domain, DomainWithPassword, DomainWithStatus } from "./domain";
|
import type { Domain, DomainWithStatus } from "./domain";
|
||||||
import type {
|
import type {
|
||||||
AppResponse,
|
AppResponse,
|
||||||
KintoneSpace,
|
|
||||||
AppDetail,
|
AppDetail,
|
||||||
FileContent,
|
FileContent,
|
||||||
} from "./kintone";
|
} from "./kintone";
|
||||||
@@ -47,11 +46,6 @@ export interface TestDomainConnectionParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Browse IPC Types ====================
|
// ==================== Browse IPC Types ====================
|
||||||
|
|
||||||
export interface GetSpacesParams {
|
|
||||||
domainId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetAppsParams {
|
export interface GetAppsParams {
|
||||||
domainId: string;
|
domainId: string;
|
||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
@@ -141,7 +135,6 @@ export interface ElectronAPI {
|
|||||||
) => Promise<Result<boolean>>;
|
) => Promise<Result<boolean>>;
|
||||||
|
|
||||||
// Browse
|
// Browse
|
||||||
getSpaces: (params: GetSpacesParams) => Promise<Result<KintoneSpace[]>>;
|
|
||||||
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
getApps: (params: GetAppsParams) => Promise<Result<AppResponse[]>>;
|
||||||
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
getAppDetail: (params: GetAppDetailParams) => Promise<Result<AppDetail>>;
|
||||||
getFileContent: (
|
getFileContent: (
|
||||||
|
|||||||
@@ -5,20 +5,33 @@
|
|||||||
|
|
||||||
import type { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
import type { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
||||||
|
|
||||||
// ==================== SDK Type Extraction ====================
|
/**
|
||||||
// Use typeof + ReturnType to extract types from SDK methods
|
* API Error - simplified from SDK's KintoneRestAPIError
|
||||||
|
*/
|
||||||
|
export interface KintoneApiError {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
id: string;
|
||||||
|
errors?: Record<string, { messages: string[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== SDK Type Extraction ==============
|
||||||
type KintoneClient = KintoneRestAPIClient;
|
type KintoneClient = KintoneRestAPIClient;
|
||||||
|
|
||||||
/** App response from getApp/getApps */
|
/** App response from getApp/getApps */
|
||||||
export type AppResponse = Awaited<ReturnType<KintoneClient["app"]["getApp"]>>;
|
export type AppResponse = Awaited<ReturnType<KintoneClient["app"]["getApp"]>>;
|
||||||
|
|
||||||
|
/** App customization request - parameters for updateAppCustomize */
|
||||||
|
export type AppCustomizeParameter = Parameters<
|
||||||
|
KintoneClient["app"]["updateAppCustomize"]
|
||||||
|
>[number];
|
||||||
|
|
||||||
/** App customization response */
|
/** App customization response */
|
||||||
export type AppCustomizeResponse = Awaited<
|
export type AppCustomizeResponse = Awaited<
|
||||||
ReturnType<KintoneClient["app"]["getAppCustomize"]>
|
ReturnType<KintoneClient["app"]["getAppCustomize"]>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// ==================== Custom Business Types ====================
|
// ============== Custom Business Types ==============
|
||||||
// These types represent business-layer aggregation or additional metadata
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App detail - combines app info + customization
|
* App detail - combines app info + customization
|
||||||
@@ -40,18 +53,48 @@ export interface FileContent {
|
|||||||
content?: string; // Base64 encoded or text
|
content?: string; // Base64 encoded or text
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* File config for customization
|
|
||||||
* Using SDK's type directly from AppCustomizeResponse
|
|
||||||
*/
|
|
||||||
export type FileConfig = NonNullable<AppCustomizeResponse["desktop"]["js"]>[number];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Error - simplified from SDK's KintoneRestAPIError
|
* File config for customization
|
||||||
|
* Using SDK's type directly from AppCustomizeResponse
|
||||||
|
*
|
||||||
|
* IMPORTANT: The Kintone API does NOT return file names in getAppCustomize response.
|
||||||
|
* - FILE type: { type: "FILE", file: { fileKey: string } }
|
||||||
|
* - URL type: { type: "URL", url: string }
|
||||||
|
*
|
||||||
|
* Use getDisplayName() utility to get user-friendly display names.
|
||||||
*/
|
*/
|
||||||
export interface KintoneApiError {
|
export type FileConfigParameter = NonNullable<NonNullable<AppCustomizeParameter["desktop"]>["js"]>[number];
|
||||||
code: string;
|
export type FileConfigResponse = NonNullable<AppCustomizeResponse["desktop"]>["js"][number];
|
||||||
message: string;
|
|
||||||
id: string;
|
// ============== Type Utilities ==============
|
||||||
errors?: Record<string, { messages: string[] }>;
|
type ExtractUrlType<T> = T extends { type: "URL" } ? T : never;
|
||||||
|
export type UrlResourceParameter = ExtractUrlType<FileConfigParameter>;
|
||||||
|
export type UrlResourceResponse = ExtractUrlType<FileConfigResponse>;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return resource.type === "URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
return resource.type === "FILE";
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/shared/utils/fileDisplay.ts
Normal file
48
src/shared/utils/fileDisplay.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { FileConfigResponse } from "@shared/types/kintone";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user-friendly display name for a file config
|
||||||
|
*/
|
||||||
|
export function getDisplayName(
|
||||||
|
file: FileConfigResponse,
|
||||||
|
fileType: "js" | "css",
|
||||||
|
index: number,
|
||||||
|
): string {
|
||||||
|
if (file.type === "URL" && file.url) {
|
||||||
|
return extractFilenameFromUrl(file.url) || `URL ${index + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type === "FILE" && file.file?.fileKey) {
|
||||||
|
return `${file.file.fileKey}.${fileType}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Unknown File ${index + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract filename from URL
|
||||||
|
*/
|
||||||
|
export function extractFilenameFromUrl(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const pathname = urlObj.pathname;
|
||||||
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
|
return segments.length > 0 ? segments[segments.length - 1] : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if file config is a FILE type upload
|
||||||
|
*/
|
||||||
|
export function isFileUpload(file: FileConfigResponse): boolean {
|
||||||
|
return file.type === "FILE" && !!file.file?.fileKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fileKey from file config (only for FILE type)
|
||||||
|
*/
|
||||||
|
export function getFileKey(file: FileConfigResponse): string | undefined {
|
||||||
|
return file.type === "FILE" ? file.file?.fileKey : undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user