Claude güncellemesi DynamicAppService değişiklikleri

This commit is contained in:
Sedat ÖZTÜRK 2026-08-15 11:29:50 +03:00
parent b4c8fc3a2b
commit c2f1b203ab
2 changed files with 221 additions and 131 deletions

View file

@ -62,13 +62,6 @@ export interface DynamicAppServiceListResult {
totalCount: number
}
export const postTestCompile = (input: TestCompileDto) =>
apiService.fetchData<CompileResult>({
method: 'POST',
url: `/api/app/dynamic-app-service/test-compile`,
data: input as any,
})
class DynamicServiceService {
private readonly baseUrl = '/api/app/dynamic-app-service'

View file

@ -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<List<string>> GetSampleDataAsync()
{
return await Task.FromResult(new List<string>
{
"Item 1",
"Item 2",
"Item 3"
return await Task.FromResult(new List<string>
{
"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<CompileResult | null>(null)
const [publishResult, setPublishResult] = useState<PublishResult | null>(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(
<Notification type="danger" duration={3000}>
{message}
</Notification>,
)
}, [])
const pushSuccess = useCallback((message: string) => {
toast.push(
<Notification type="success" duration={2000}>
{message}
</Notification>,
)
}, [])
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<CompileResult> => {
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 (
<div className="flex items-center justify-center h-64">
@ -190,8 +278,6 @@ const DynamicServiceEditor: React.FC = () => {
)
}
const serviceNameError = submitted && !serviceName.trim()
return (
<div className="space-y-4">
<PageTitle title={pageTitle} />
@ -216,16 +302,16 @@ const DynamicServiceEditor: React.FC = () => {
</div>
</div>
{/* Right: action buttons + swagger + publish */}
{/* Right: action buttons */}
<div className="flex items-center gap-2 pr-3">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServices}
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 px-4 py-2 rounded-lg hover:text-slate-800 dark:hover:text-gray-100 transition-colors"
>
<FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DynamicServicesEditor.BackToServices')}
</Link>
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700" />
<Button
type="button"
onClick={copyCode}
@ -238,7 +324,7 @@ const DynamicServiceEditor: React.FC = () => {
<Button
type="button"
onClick={handleTestCompile}
disabled={isCompiling || !code.trim()}
disabled={isBusy || !code.trim()}
variant="solid"
icon={
isCompiling ? (
@ -256,7 +342,7 @@ const DynamicServiceEditor: React.FC = () => {
<Button
type="button"
onClick={handlePublish}
disabled={isPublishing}
disabled={isBusy || !code.trim()}
variant="solid"
color="green-500"
icon={
@ -295,15 +381,22 @@ const DynamicServiceEditor: React.FC = () => {
? translate('::App.DynamicServicesEditor.CompileSuccess')
: translate('::App.DynamicServicesEditor.CompileFailed')}
</span>
{!compileResult.success && compileResult.errors && compileResult.errors.length > 0 && (
<ul className="mt-1 space-y-0.5">
{compileResult.errors.map((e, i) => (
<li key={i} className="text-xs font-mono">
[{e.code}] {translate('::App.DynamicServicesEditor.Line')}{' '}
{e.line}: {e.message}
</li>
))}
</ul>
{!compileResult.success && (
<>
{compileResult.errorMessage && !compileResult.errors?.length && (
<p className="text-xs mt-0.5">{compileResult.errorMessage}</p>
)}
{!!compileResult.errors?.length && (
<ul className="mt-1 space-y-0.5">
{compileResult.errors.map((e, i) => (
<li key={`${e.code}-${e.line}-${i}`} className="text-xs font-mono">
[{e.code}] {translate('::App.DynamicServicesEditor.Line')}{' '}
{e.line}: {e.message}
</li>
))}
</ul>
)}
</>
)}
</div>
<span className="text-xs text-slate-400 shrink-0">
@ -330,7 +423,6 @@ const DynamicServiceEditor: React.FC = () => {
<div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start">
{/* LEFT PANEL — Servis Ayarları */}
<div className="w-full xl:w-1/4 shrink-0 bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 p-5 space-y-4">
{/* Panel header */}
<div className="flex items-center gap-2 pb-3 border-b border-slate-100 dark:border-gray-700">
<FaCog className="w-4 h-4 text-blue-500" />
<h2 className="font-semibold text-slate-700 dark:text-gray-100 text-sm">
@ -340,24 +432,21 @@ const DynamicServiceEditor: React.FC = () => {
{/* Servis Adı */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1">
<label className={LABEL_CLASS} htmlFor="dse-service-name">
{translate('::App.DynamicServicesEditor.ServiceName')}
</label>
<input
id="dse-service-name"
type="text"
value={serviceName}
onChange={(e) => {
setServiceName(e.target.value)
setSubmitted(false)
}}
onChange={(e) => setServiceName(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.ServiceNamePlaceholder',
)}
className={`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 ${
serviceNameError
? 'border-red-500 bg-red-50 dark:bg-red-900/20'
: 'border-slate-300 dark:border-gray-700'
className={`${INPUT_BASE_CLASS} ${
serviceNameError ? INPUT_ERROR_BORDER : INPUT_IDLE_BORDER
}`}
aria-invalid={serviceNameError}
autoFocus
/>
{serviceNameError && (
@ -369,70 +458,76 @@ const DynamicServiceEditor: React.FC = () => {
{/* Görünen Ad */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1">
<label className={LABEL_CLASS} htmlFor="dse-display-name">
{translate('::App.Platform.DisplayName')}
</label>
<input
id="dse-display-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.DisplayNamePlaceholder',
)}
className="w-full px-3 py-2 border border-slate-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Açıklama */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1">
<label className={LABEL_CLASS} htmlFor="dse-description">
{translate('::App.Listform.ListformField.Description')}
</label>
<input
id="dse-description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.DescriptionPlaceholder',
)}
className="w-full px-3 py-2 border border-slate-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Ana Entity Türü */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1">
<label className={LABEL_CLASS} htmlFor="dse-primary-entity">
{translate('::App.DynamicServicesEditor.PrimaryEntityType')}
</label>
<input
id="dse-primary-entity"
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.PrimaryEntityTypePlaceholder',
)}
className="w-full px-3 py-2 border border-slate-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Aktif */}
<div>
<label className="block text-sm font-medium mb-1">
{translate('::App.Listform.ListformField.IsActive')}
</label>
<div className="flex items-center gap-2">
<input
id="dse-is-active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded accent-blue-600 cursor-pointer"
/>
<label
className="text-sm font-medium text-slate-700 dark:text-gray-300 cursor-pointer"
htmlFor="dse-is-active"
>
{translate('::App.Listform.ListformField.IsActive')}
</label>
</div>
</div>
{/* RIGHT PANEL — Önizleme + Editor */}
<div className="w-full flex-1 min-w-0 space-y-4">
{/* Monaco Editor */}
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
{/* RIGHT PANEL — Editor */}
<div className="w-full flex-1 min-w-0">
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="px-5 py-3 bg-slate-50 dark:bg-gray-800 border-b border-slate-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-2">
<FaCode className="w-4 h-4 text-slate-500 dark:text-gray-400" />
@ -442,22 +537,24 @@ const DynamicServiceEditor: React.FC = () => {
</div>
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-gray-400">
<span>
{translate('::App.DynamicServicesEditor.LineCount')}{' '}
{code.split('\n').length}
{translate('::App.DynamicServicesEditor.LineCount')} {lineCount}
</span>
<span className="text-slate-300">|</span>
<span className="text-slate-300 dark:text-gray-600">|</span>
<span>
{translate('::App.DynamicServicesEditor.CharCount')} {code.length}
{translate('::App.DynamicServicesEditor.CharCount')} {charCount}
</span>
</div>
</div>
<div style={{ height: '560px' }}>
<div className="h-[560px]">
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value || '')}
options={editorOptions}
theme="vs-dark"
onChange={(value) => setCode(value ?? '')}
options={EDITOR_OPTIONS}
theme={mode === 'dark' ? 'vs-dark' : 'light'}
loading={
<FaSpinner className="w-6 h-6 animate-spin text-slate-400" />
}
/>
</div>
</div>