diff --git a/ui/src/services/dynamicService.service.ts b/ui/src/services/dynamicService.service.ts index 63096d17..dc7663c5 100644 --- a/ui/src/services/dynamicService.service.ts +++ b/ui/src/services/dynamicService.service.ts @@ -62,13 +62,6 @@ export interface DynamicAppServiceListResult { totalCount: number } -export const postTestCompile = (input: TestCompileDto) => - apiService.fetchData({ - method: 'POST', - url: `/api/app/dynamic-app-service/test-compile`, - data: input as any, - }) - class DynamicServiceService { private readonly baseUrl = '/api/app/dynamic-app-service' diff --git a/ui/src/views/developerKit/DynamicServiceEditor.tsx b/ui/src/views/developerKit/DynamicServiceEditor.tsx index 3082e03e..250c0a09 100644 --- a/ui/src/views/developerKit/DynamicServiceEditor.tsx +++ b/ui/src/views/developerKit/DynamicServiceEditor.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' import { Editor } from '@monaco-editor/react' +import type { editor } from 'monaco-editor' import { FaPlay, FaCopy, @@ -17,14 +18,14 @@ import { dynamicServiceService, type CompileResult, type PublishResult, - postTestCompile, - type TestCompileDto, } from '@/services/dynamicService.service' import PageTitle from '@/components/shared/PageTitle' import { ROUTES_ENUM } from '@/routes/route.constant' import Button from '@/components/ui/Button' +import { Notification, toast } from '@/components/ui' +import { useConfig } from '@/components/ui/ConfigProvider' -const defaultTemplate = `using System; +const DEFAULT_TEMPLATE = `using System; using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Services; @@ -43,22 +44,52 @@ namespace DynamicServices public virtual async Task> GetSampleDataAsync() { - return await Task.FromResult(new List - { - "Item 1", - "Item 2", - "Item 3" + return await Task.FromResult(new List + { + "Item 1", + "Item 2", + "Item 3" }); } } }` +const EDITOR_OPTIONS: editor.IStandaloneEditorConstructionOptions = { + fontSize: 14, + lineNumbers: 'on', + roundedSelection: false, + scrollBeyondLastLine: false, + automaticLayout: true, + minimap: { enabled: false }, + folding: true, + wordWrap: 'on', +} + +const INPUT_BASE_CLASS = + 'w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' +const INPUT_IDLE_BORDER = 'border-slate-300 dark:border-gray-700' +const INPUT_ERROR_BORDER = 'border-red-500 bg-red-50 dark:bg-red-900/20' +const LABEL_CLASS = + 'block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1' + +const getErrorMessage = (error: unknown, fallback: string): string => { + const response = (error as { response?: { data?: { error?: { message?: string }; message?: string } } }) + ?.response + return ( + response?.data?.error?.message || + response?.data?.message || + (error instanceof Error ? error.message : undefined) || + fallback + ) +} + const DynamicServiceEditor: React.FC = () => { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { translate } = useLocalization() + const { mode } = useConfig() - const [code, setCode] = useState(defaultTemplate) + const [code, setCode] = useState(DEFAULT_TEMPLATE) const [serviceName, setServiceName] = useState('') const [displayName, setDisplayName] = useState('') const [description, setDescription] = useState('') @@ -68,120 +99,177 @@ const DynamicServiceEditor: React.FC = () => { const [isCompiling, setIsCompiling] = useState(false) const [isPublishing, setIsPublishing] = useState(false) - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(Boolean(id)) const [compileResult, setCompileResult] = useState(null) const [publishResult, setPublishResult] = useState(null) - const editorOptions = { - fontSize: 14, - lineNumbers: 'on' as const, - roundedSelection: false, - scrollBeyondLastLine: false, - automaticLayout: true, - minimap: { enabled: false }, - folding: true, - wordWrap: 'on' as const, - } + const pushError = useCallback((message: string) => { + toast.push( + + {message} + , + ) + }, []) + + const pushSuccess = useCallback((message: string) => { + toast.push( + + {message} + , + ) + }, []) useEffect(() => { - if (id) { - loadService(id) - } - }, [id]) - - const loadService = async (serviceId: string) => { - try { - setIsLoading(true) - const data = await dynamicServiceService.getById(serviceId) - setCode(data.code) - setServiceName(data.name) - setDisplayName(data.displayName || '') - setDescription(data.description || '') - setPrimaryEntityType(data.primaryEntityType || '') - setIsActive(data.isActive ?? true) - } catch (error) { - console.error('Servis yüklenirken hata:', error) - } finally { - setIsLoading(false) - } - } - - const handleTestCompile = async () => { - if (!code.trim()) { - alert(translate('::App.DynamicServicesEditor.PleaseEnterCode')) + if (!id) { return } + let cancelled = false + + const loadService = async () => { + try { + setIsLoading(true) + const data = await dynamicServiceService.getById(id) + if (cancelled) return + setCode(data.code || DEFAULT_TEMPLATE) + setServiceName(data.name) + setDisplayName(data.displayName || '') + setDescription(data.description || '') + setPrimaryEntityType(data.primaryEntityType || '') + setIsActive(data.isActive ?? true) + } catch (error) { + if (cancelled) return + pushError(getErrorMessage(error, translate('::App.Platform.ErrorLoading'))) + } finally { + if (!cancelled) setIsLoading(false) + } + } + + loadService() + return () => { + cancelled = true + } + }, [id, pushError, translate]) + + const runCompile = useCallback(async (): Promise => { + setIsCompiling(true) + setCompileResult(null) try { - setIsCompiling(true) - setCompileResult(null) - const result = await postTestCompile({ code } as TestCompileDto) - setCompileResult(result.data) - } catch (error: any) { - setCompileResult({ + const result = await dynamicServiceService.testCompile({ code }) + setCompileResult(result) + return result + } catch (error) { + const failed: CompileResult = { success: false, - errorMessage: - error.response?.data?.message || + errorMessage: getErrorMessage( + error, translate('::App.DynamicServicesEditor.CompileError'), + ), compilationTimeMs: 0, hasWarnings: false, errors: [], - }) + } + setCompileResult(failed) + return failed } finally { setIsCompiling(false) } - } + }, [code, translate]) - const handlePublish = async () => { - setSubmitted(true) - if (!code.trim() || !serviceName.trim()) { + const handleTestCompile = useCallback(async () => { + if (isCompiling || isPublishing) return + if (!code.trim()) { + pushError(translate('::App.DynamicServicesEditor.PleaseEnterCode')) return } - if (isPublishing) return - try { - setIsPublishing(true) - setPublishResult(null) + await runCompile() + }, [code, isCompiling, isPublishing, pushError, runCompile, translate]) - // Edit modunda: önce eskiyi sil, sonra yeniden yayınla + const handlePublish = useCallback(async () => { + if (isPublishing || isCompiling) return + + setSubmitted(true) + const name = serviceName.trim() + if (!code.trim() || !name) { + return + } + + setIsPublishing(true) + setPublishResult(null) + try { + // Edit modunda eski servis silinip yeniden yayınlanır; silmeden önce + // kodun derlendiğinden emin ol ki başarısız derlemede servis kaybolmasın. if (id) { + const compiled = await runCompile() + if (!compiled.success) { + return + } await dynamicServiceService.delete(id) } const result = await dynamicServiceService.publish({ - name: serviceName, + name, code, - displayName, - description, - primaryEntityType, + displayName: displayName.trim(), + description: description.trim(), + primaryEntityType: primaryEntityType.trim(), isActive, }) if (result.success) { + pushSuccess(translate('::App.Platform.SavedSuccessfully')) navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices) } else { setPublishResult(result) } - } catch (error: any) { + } catch (error) { setPublishResult({ success: false, - errorMessage: - error.response?.data?.message || + errorMessage: getErrorMessage( + error, translate('::App.DynamicServicesEditor.PublishError'), + ), }) } finally { setIsPublishing(false) } - } + }, [ + code, + description, + displayName, + id, + isActive, + isCompiling, + isPublishing, + navigate, + primaryEntityType, + pushSuccess, + runCompile, + serviceName, + translate, + ]) - const copyCode = () => { - navigator.clipboard.writeText(code) - alert(translate('::App.DynamicServicesEditor.CodeCopied')) - } + const copyCode = useCallback(async () => { + try { + await navigator.clipboard.writeText(code) + pushSuccess(translate('::App.DynamicServicesEditor.CodeCopied')) + } catch (error) { + pushError(getErrorMessage(error, translate('::App.Platform.Error'))) + } + }, [code, pushError, pushSuccess, translate]) const pageTitle = id ? translate('::App.DynamicServicesEditor.EditTitle') : translate('::App.DeveloperKitDynamicServices.NewService') + const { lineCount, charCount } = useMemo( + () => ({ lineCount: code.split('\n').length, charCount: code.length }), + [code], + ) + + const serviceNameError = submitted && !serviceName.trim() + const isBusy = isCompiling || isPublishing + if (isLoading) { return (
@@ -190,8 +278,6 @@ const DynamicServiceEditor: React.FC = () => { ) } - const serviceNameError = submitted && !serviceName.trim() - return (
@@ -216,16 +302,16 @@ const DynamicServiceEditor: React.FC = () => {
- {/* Right: action buttons + swagger + publish */} + {/* Right: action buttons */}
{translate('::App.DynamicServicesEditor.BackToServices')} -
+
@@ -330,7 +423,6 @@ const DynamicServiceEditor: React.FC = () => {
{/* LEFT PANEL — Servis Ayarları */}
- {/* Panel header */}

@@ -340,24 +432,21 @@ const DynamicServiceEditor: React.FC = () => { {/* Servis Adı */}
-