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 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 { class DynamicServiceService {
private readonly baseUrl = '/api/app/dynamic-app-service' 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 { useParams, useNavigate, Link } from 'react-router-dom'
import { Editor } from '@monaco-editor/react' import { Editor } from '@monaco-editor/react'
import type { editor } from 'monaco-editor'
import { import {
FaPlay, FaPlay,
FaCopy, FaCopy,
@ -17,14 +18,14 @@ import {
dynamicServiceService, dynamicServiceService,
type CompileResult, type CompileResult,
type PublishResult, type PublishResult,
postTestCompile,
type TestCompileDto,
} from '@/services/dynamicService.service' } from '@/services/dynamicService.service'
import PageTitle from '@/components/shared/PageTitle' import PageTitle from '@/components/shared/PageTitle'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import Button from '@/components/ui/Button' 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
@ -53,12 +54,42 @@ namespace DynamicServices
} }
}` }`
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 DynamicServiceEditor: React.FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization() const { translate } = useLocalization()
const { mode } = useConfig()
const [code, setCode] = useState(defaultTemplate) const [code, setCode] = useState(DEFAULT_TEMPLATE)
const [serviceName, setServiceName] = useState('') const [serviceName, setServiceName] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
@ -68,120 +99,177 @@ const DynamicServiceEditor: React.FC = () => {
const [isCompiling, setIsCompiling] = useState(false) const [isCompiling, setIsCompiling] = useState(false)
const [isPublishing, setIsPublishing] = 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 [compileResult, setCompileResult] = useState<CompileResult | null>(null)
const [publishResult, setPublishResult] = useState<PublishResult | null>(null) const [publishResult, setPublishResult] = useState<PublishResult | null>(null)
const editorOptions = { const pushError = useCallback((message: string) => {
fontSize: 14, toast.push(
lineNumbers: 'on' as const, <Notification type="danger" duration={3000}>
roundedSelection: false, {message}
scrollBeyondLastLine: false, </Notification>,
automaticLayout: true, )
minimap: { enabled: false }, }, [])
folding: true,
wordWrap: 'on' as const, const pushSuccess = useCallback((message: string) => {
} toast.push(
<Notification type="success" duration={2000}>
{message}
</Notification>,
)
}, [])
useEffect(() => { useEffect(() => {
if (id) { 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'))
return 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 { try {
setIsCompiling(true) const result = await dynamicServiceService.testCompile({ code })
setCompileResult(null) setCompileResult(result)
const result = await postTestCompile({ code } as TestCompileDto) return result
setCompileResult(result.data) } catch (error) {
} catch (error: any) { const failed: CompileResult = {
setCompileResult({
success: false, success: false,
errorMessage: errorMessage: getErrorMessage(
error.response?.data?.message || error,
translate('::App.DynamicServicesEditor.CompileError'), translate('::App.DynamicServicesEditor.CompileError'),
),
compilationTimeMs: 0, compilationTimeMs: 0,
hasWarnings: false, hasWarnings: false,
errors: [], errors: [],
}) }
setCompileResult(failed)
return failed
} finally { } finally {
setIsCompiling(false) setIsCompiling(false)
} }
} }, [code, translate])
const handlePublish = async () => { const handleTestCompile = useCallback(async () => {
setSubmitted(true) if (isCompiling || isPublishing) return
if (!code.trim() || !serviceName.trim()) { if (!code.trim()) {
pushError(translate('::App.DynamicServicesEditor.PleaseEnterCode'))
return return
} }
if (isPublishing) return await runCompile()
try { }, [code, isCompiling, isPublishing, pushError, runCompile, translate])
setIsPublishing(true)
setPublishResult(null)
// 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) { if (id) {
const compiled = await runCompile()
if (!compiled.success) {
return
}
await dynamicServiceService.delete(id) await dynamicServiceService.delete(id)
} }
const result = await dynamicServiceService.publish({ const result = await dynamicServiceService.publish({
name: serviceName, name,
code, code,
displayName, displayName: displayName.trim(),
description, description: description.trim(),
primaryEntityType, primaryEntityType: primaryEntityType.trim(),
isActive, isActive,
}) })
if (result.success) { if (result.success) {
pushSuccess(translate('::App.Platform.SavedSuccessfully'))
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices) navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
} else { } else {
setPublishResult(result) setPublishResult(result)
} }
} catch (error: any) { } catch (error) {
setPublishResult({ setPublishResult({
success: false, success: false,
errorMessage: errorMessage: getErrorMessage(
error.response?.data?.message || error,
translate('::App.DynamicServicesEditor.PublishError'), translate('::App.DynamicServicesEditor.PublishError'),
),
}) })
} finally { } finally {
setIsPublishing(false) setIsPublishing(false)
} }
} }, [
code,
description,
displayName,
id,
isActive,
isCompiling,
isPublishing,
navigate,
primaryEntityType,
pushSuccess,
runCompile,
serviceName,
translate,
])
const copyCode = () => { const copyCode = useCallback(async () => {
navigator.clipboard.writeText(code) try {
alert(translate('::App.DynamicServicesEditor.CodeCopied')) 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 const pageTitle = id
? translate('::App.DynamicServicesEditor.EditTitle') ? translate('::App.DynamicServicesEditor.EditTitle')
: translate('::App.DeveloperKitDynamicServices.NewService') : 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) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@ -190,8 +278,6 @@ const DynamicServiceEditor: React.FC = () => {
) )
} }
const serviceNameError = submitted && !serviceName.trim()
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<PageTitle title={pageTitle} /> <PageTitle title={pageTitle} />
@ -216,16 +302,16 @@ const DynamicServiceEditor: React.FC = () => {
</div> </div>
</div> </div>
{/* Right: action buttons + swagger + publish */} {/* Right: action buttons */}
<div className="flex items-center gap-2 pr-3"> <div className="flex items-center gap-2 pr-3">
<Link <Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServices} 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" /> <FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DynamicServicesEditor.BackToServices')} {translate('::App.DynamicServicesEditor.BackToServices')}
</Link> </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 <Button
type="button" type="button"
onClick={copyCode} onClick={copyCode}
@ -238,7 +324,7 @@ const DynamicServiceEditor: React.FC = () => {
<Button <Button
type="button" type="button"
onClick={handleTestCompile} onClick={handleTestCompile}
disabled={isCompiling || !code.trim()} disabled={isBusy || !code.trim()}
variant="solid" variant="solid"
icon={ icon={
isCompiling ? ( isCompiling ? (
@ -256,7 +342,7 @@ const DynamicServiceEditor: React.FC = () => {
<Button <Button
type="button" type="button"
onClick={handlePublish} onClick={handlePublish}
disabled={isPublishing} disabled={isBusy || !code.trim()}
variant="solid" variant="solid"
color="green-500" color="green-500"
icon={ icon={
@ -295,15 +381,22 @@ const DynamicServiceEditor: React.FC = () => {
? translate('::App.DynamicServicesEditor.CompileSuccess') ? translate('::App.DynamicServicesEditor.CompileSuccess')
: translate('::App.DynamicServicesEditor.CompileFailed')} : translate('::App.DynamicServicesEditor.CompileFailed')}
</span> </span>
{!compileResult.success && compileResult.errors && compileResult.errors.length > 0 && ( {!compileResult.success && (
<ul className="mt-1 space-y-0.5"> <>
{compileResult.errors.map((e, i) => ( {compileResult.errorMessage && !compileResult.errors?.length && (
<li key={i} className="text-xs font-mono"> <p className="text-xs mt-0.5">{compileResult.errorMessage}</p>
[{e.code}] {translate('::App.DynamicServicesEditor.Line')}{' '} )}
{e.line}: {e.message} {!!compileResult.errors?.length && (
</li> <ul className="mt-1 space-y-0.5">
))} {compileResult.errors.map((e, i) => (
</ul> <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> </div>
<span className="text-xs text-slate-400 shrink-0"> <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"> <div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start">
{/* LEFT PANEL — Servis Ayarları */} {/* 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"> <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"> <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" /> <FaCog className="w-4 h-4 text-blue-500" />
<h2 className="font-semibold text-slate-700 dark:text-gray-100 text-sm"> <h2 className="font-semibold text-slate-700 dark:text-gray-100 text-sm">
@ -340,24 +432,21 @@ const DynamicServiceEditor: React.FC = () => {
{/* Servis Adı */} {/* Servis Adı */}
<div> <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')} {translate('::App.DynamicServicesEditor.ServiceName')}
</label> </label>
<input <input
id="dse-service-name"
type="text" type="text"
value={serviceName} value={serviceName}
onChange={(e) => { onChange={(e) => setServiceName(e.target.value)}
setServiceName(e.target.value)
setSubmitted(false)
}}
placeholder={translate( placeholder={translate(
'::App.DynamicServicesEditor.ServiceNamePlaceholder', '::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 ${ className={`${INPUT_BASE_CLASS} ${
serviceNameError serviceNameError ? INPUT_ERROR_BORDER : INPUT_IDLE_BORDER
? 'border-red-500 bg-red-50 dark:bg-red-900/20'
: 'border-slate-300 dark:border-gray-700'
}`} }`}
aria-invalid={serviceNameError}
autoFocus autoFocus
/> />
{serviceNameError && ( {serviceNameError && (
@ -369,70 +458,76 @@ const DynamicServiceEditor: React.FC = () => {
{/* Görünen Ad */} {/* Görünen Ad */}
<div> <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')} {translate('::App.Platform.DisplayName')}
</label> </label>
<input <input
id="dse-display-name"
type="text" type="text"
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate( placeholder={translate(
'::App.DynamicServicesEditor.DisplayNamePlaceholder', '::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> </div>
{/* Açıklama */} {/* Açıklama */}
<div> <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')} {translate('::App.Listform.ListformField.Description')}
</label> </label>
<input <input
id="dse-description"
type="text" type="text"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder={translate( placeholder={translate(
'::App.DynamicServicesEditor.DescriptionPlaceholder', '::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> </div>
{/* Ana Entity Türü */} {/* Ana Entity Türü */}
<div> <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')} {translate('::App.DynamicServicesEditor.PrimaryEntityType')}
</label> </label>
<input <input
id="dse-primary-entity"
type="text" type="text"
value={primaryEntityType} value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)} onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate( placeholder={translate(
'::App.DynamicServicesEditor.PrimaryEntityTypePlaceholder', '::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> </div>
{/* Aktif */} {/* Aktif */}
<div> <div className="flex items-center gap-2">
<label className="block text-sm font-medium mb-1">
{translate('::App.Listform.ListformField.IsActive')}
</label>
<input <input
id="dse-is-active"
type="checkbox" type="checkbox"
checked={isActive} checked={isActive}
onChange={(e) => setIsActive(e.target.checked)} onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded accent-blue-600 cursor-pointer" 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>
</div> </div>
{/* RIGHT PANEL — Önizleme + Editor */} {/* RIGHT PANEL — Editor */}
<div className="w-full flex-1 min-w-0 space-y-4"> <div className="w-full flex-1 min-w-0">
{/* Monaco Editor */} <div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="bg-white rounded-lg border border-slate-200 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="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"> <div className="flex items-center gap-2">
<FaCode className="w-4 h-4 text-slate-500 dark:text-gray-400" /> <FaCode className="w-4 h-4 text-slate-500 dark:text-gray-400" />
@ -442,22 +537,24 @@ const DynamicServiceEditor: React.FC = () => {
</div> </div>
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-gray-400"> <div className="flex items-center gap-2 text-xs text-slate-500 dark:text-gray-400">
<span> <span>
{translate('::App.DynamicServicesEditor.LineCount')}{' '} {translate('::App.DynamicServicesEditor.LineCount')} {lineCount}
{code.split('\n').length}
</span> </span>
<span className="text-slate-300">|</span> <span className="text-slate-300 dark:text-gray-600">|</span>
<span> <span>
{translate('::App.DynamicServicesEditor.CharCount')} {code.length} {translate('::App.DynamicServicesEditor.CharCount')} {charCount}
</span> </span>
</div> </div>
</div> </div>
<div style={{ height: '560px' }}> <div className="h-[560px]">
<Editor <Editor
defaultLanguage="csharp" defaultLanguage="csharp"
value={code} value={code}
onChange={(value) => setCode(value || '')} onChange={(value) => setCode(value ?? '')}
options={editorOptions} options={EDITOR_OPTIONS}
theme="vs-dark" theme={mode === 'dark' ? 'vs-dark' : 'light'}
loading={
<FaSpinner className="w-6 h-6 animate-spin text-slate-400" />
}
/> />
</div> </div>
</div> </div>