Eksik LanguageText keyler tanımlandı

This commit is contained in:
Sedat ÖZTÜRK 2026-08-14 14:32:31 +03:00
parent b5cba69e28
commit f372c17249
166 changed files with 17380 additions and 8509 deletions

View file

@ -1,13 +1,13 @@
import React, { useState, useRef, useCallback } from "react"; import React, { useState, useRef, useCallback } from 'react'
interface SplitterProps { interface SplitterProps {
direction: "horizontal" | "vertical"; direction: 'horizontal' | 'vertical'
initialSize: number; initialSize: number
minSize?: number; minSize?: number
maxSize?: number; maxSize?: number
children: [React.ReactNode, React.ReactNode]; children: [React.ReactNode, React.ReactNode]
className?: string; className?: string
reverse?: boolean; // İkinci panel için boyut kontrolü reverse?: boolean // İkinci panel için boyut kontrolü
} }
export const Splitter: React.FC<SplitterProps> = ({ export const Splitter: React.FC<SplitterProps> = ({
@ -16,66 +16,66 @@ export const Splitter: React.FC<SplitterProps> = ({
minSize = 200, minSize = 200,
maxSize = 800, maxSize = 800,
children, children,
className = "", className = '',
reverse = false, reverse = false,
}) => { }) => {
const [size, setSize] = useState(initialSize); const [size, setSize] = useState(initialSize)
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false)
const splitterRef = useRef<HTMLDivElement>(null); const splitterRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null)
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault()
setIsDragging(true); setIsDragging(true)
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!splitterRef.current || !containerRef.current) return; if (!splitterRef.current || !containerRef.current) return
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect()
let newSize: number; let newSize: number
if (direction === "horizontal") { if (direction === 'horizontal') {
if (reverse) { if (reverse) {
// İkinci panel için boyut kontrolü - sağdan ölçüm // İkinci panel için boyut kontrolü - sağdan ölçüm
newSize = rect.right - e.clientX; newSize = rect.right - e.clientX
} else { } else {
// İlk panel için boyut kontrolü - soldan ölçüm // İlk panel için boyut kontrolü - soldan ölçüm
newSize = e.clientX - rect.left; newSize = e.clientX - rect.left
} }
} else { } else {
if (reverse) { if (reverse) {
// İkinci panel için boyut kontrolü - alttan ölçüm // İkinci panel için boyut kontrolü - alttan ölçüm
newSize = rect.bottom - e.clientY; newSize = rect.bottom - e.clientY
} else { } else {
// İlk panel için boyut kontrolü - üstten ölçüm // İlk panel için boyut kontrolü - üstten ölçüm
newSize = e.clientY - rect.top; newSize = e.clientY - rect.top
} }
} }
newSize = Math.max(minSize, Math.min(maxSize, newSize)); newSize = Math.max(minSize, Math.min(maxSize, newSize))
setSize(newSize); setSize(newSize)
}; }
const handleMouseUp = () => { const handleMouseUp = () => {
setIsDragging(false); setIsDragging(false)
document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener('mouseup', handleMouseUp)
}; }
document.addEventListener("mousemove", handleMouseMove); document.addEventListener('mousemove', handleMouseMove)
document.addEventListener("mouseup", handleMouseUp); document.addEventListener('mouseup', handleMouseUp)
}, },
[direction, minSize, maxSize, reverse] [direction, minSize, maxSize, reverse],
); )
const isHorizontal = direction === "horizontal"; const isHorizontal = direction === 'horizontal'
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={`flex flex-1 min-h-0 ${ className={`flex flex-1 min-h-0 ${
isHorizontal ? "flex-row" : "flex-col" isHorizontal ? 'flex-row' : 'flex-col'
} h-full w-full ${className}`} } h-full w-full ${className}`}
> >
{reverse ? ( {reverse ? (
@ -85,20 +85,18 @@ export const Splitter: React.FC<SplitterProps> = ({
<div <div
ref={splitterRef} ref={splitterRef}
className={` className={`
${ ${isHorizontal ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
isHorizontal ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"
}
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0 bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
${isDragging ? "bg-blue-500 dark:bg-blue-600" : ""} ${isDragging ? 'bg-blue-500 dark:bg-blue-600' : ''}
`} `}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
/> />
<div <div
style={{ style={{
[isHorizontal ? "width" : "height"]: `${size}px`, [isHorizontal ? 'width' : 'height']: `${size}px`,
[isHorizontal ? "minWidth" : "minHeight"]: `${size}px`, [isHorizontal ? 'minWidth' : 'minHeight']: `${size}px`,
[isHorizontal ? "maxWidth" : "maxHeight"]: `${size}px`, [isHorizontal ? 'maxWidth' : 'maxHeight']: `${size}px`,
}} }}
className="overflow-hidden" className="overflow-hidden"
> >
@ -109,9 +107,9 @@ export const Splitter: React.FC<SplitterProps> = ({
<> <>
<div <div
style={{ style={{
[isHorizontal ? "width" : "height"]: `${size}px`, [isHorizontal ? 'width' : 'height']: `${size}px`,
[isHorizontal ? "minWidth" : "minHeight"]: `${size}px`, [isHorizontal ? 'minWidth' : 'minHeight']: `${size}px`,
[isHorizontal ? "maxWidth" : "maxHeight"]: `${size}px`, [isHorizontal ? 'maxWidth' : 'maxHeight']: `${size}px`,
}} }}
className="overflow-hidden" className="overflow-hidden"
> >
@ -121,11 +119,9 @@ export const Splitter: React.FC<SplitterProps> = ({
<div <div
ref={splitterRef} ref={splitterRef}
className={` className={`
${ ${isHorizontal ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
isHorizontal ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"
}
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0 bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
${isDragging ? "bg-blue-500 dark:bg-blue-600" : ""} ${isDragging ? 'bg-blue-500 dark:bg-blue-600' : ''}
`} `}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
/> />
@ -134,5 +130,5 @@ export const Splitter: React.FC<SplitterProps> = ({
</> </>
)} )}
</div> </div>
); )
}; }

View file

@ -1,6 +1,7 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa' import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface StyleModalProps { interface StyleModalProps {
isOpen: boolean isOpen: boolean
@ -12,30 +13,348 @@ interface StyleModalProps {
} }
const STYLE_GROUPS = { const STYLE_GROUPS = {
Display: ['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'contents', 'hidden'], Display: [
Layout: ['container', 'box-border', 'box-content', 'overflow-auto', 'overflow-hidden', 'overflow-visible', 'overflow-scroll', 'overflow-x-auto', 'overflow-y-auto'], 'block',
Position: ['static', 'fixed', 'absolute', 'relative', 'sticky', 'inset-0', 'top-0', 'right-0', 'bottom-0', 'left-0', 'z-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'], 'inline-block',
Flexbox: ['flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse', 'flex-wrap', 'flex-nowrap', 'grow', 'grow-0', 'shrink', 'shrink-0', 'items-start', 'items-center', 'items-end', 'items-stretch', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'justify-evenly', 'self-auto', 'self-start', 'self-center', 'self-end', 'self-stretch'], 'inline',
Grid: ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5', 'grid-cols-6', 'grid-cols-12', 'col-auto', 'col-span-1', 'col-span-2', 'col-span-3', 'col-span-4', 'col-span-6', 'col-span-12', 'grid-rows-1', 'grid-rows-2', 'grid-rows-3', 'row-span-1', 'row-span-2', 'row-span-3'], 'flex',
Spacing: ['m-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-auto', 'mx-auto', 'my-auto', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mr-1', 'mr-2', 'mr-4', 'mb-1', 'mb-2', 'mb-4', 'ml-1', 'ml-2', 'ml-4', 'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'px-1', 'px-2', 'px-3', 'px-4', 'px-6', 'py-1', 'py-2', 'py-3', 'py-4', 'py-6', 'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'], 'inline-flex',
Sizing: ['w-0', 'w-auto', 'w-full', 'w-screen', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4', 'h-0', 'h-auto', 'h-full', 'h-screen', 'min-w-0', 'min-w-full', 'min-h-0', 'min-h-full', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-full', 'max-h-full', 'aspect-square', 'aspect-video'], 'grid',
Typography: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'font-thin', 'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'italic', 'not-italic', 'underline', 'line-through', 'no-underline', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize', 'truncate', 'whitespace-normal', 'whitespace-nowrap', 'break-words', 'leading-none', 'leading-tight', 'leading-normal', 'tracking-tight', 'tracking-normal', 'tracking-wide'], 'inline-grid',
Colors: ['text-transparent', 'text-white', 'text-black', 'text-slate-400', 'text-slate-500', 'text-slate-600', 'text-slate-700', 'text-slate-800', 'text-slate-900', 'text-sky-500', 'text-blue-600', 'text-red-500', 'text-amber-500', 'text-emerald-500', 'bg-transparent', 'bg-white', 'bg-black', 'bg-slate-50', 'bg-slate-100', 'bg-slate-200', 'bg-slate-700', 'bg-slate-800', 'bg-slate-900', 'bg-sky-500', 'bg-blue-600', 'bg-red-500', 'bg-amber-500', 'bg-emerald-500'], 'contents',
Border: ['border', 'border-0', 'border-2', 'border-t', 'border-r', 'border-b', 'border-l', 'border-transparent', 'border-slate-200', 'border-slate-300', 'border-slate-700', 'border-sky-500', 'rounded-none', 'rounded-sm', 'rounded', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full', 'divide-x', 'divide-y'], 'hidden',
Effects: ['shadow-none', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'blur-sm', 'blur', 'backdrop-blur-sm', 'backdrop-blur', 'transition', 'transition-all', 'duration-150', 'duration-300', 'ease-in', 'ease-out', 'ease-in-out'], ],
Interaction: ['cursor-auto', 'cursor-default', 'cursor-pointer', 'cursor-not-allowed', 'select-none', 'select-text', 'pointer-events-none', 'pointer-events-auto', 'resize', 'resize-none', 'outline-none', 'focus:outline-none', 'hover:opacity-80', 'hover:bg-slate-100', 'focus:ring-2', 'focus:ring-sky-500'], Layout: [
Responsive: ['sm:block', 'sm:flex', 'sm:grid', 'sm:hidden', 'md:block', 'md:flex', 'md:grid', 'md:hidden', 'lg:block', 'lg:flex', 'lg:grid', 'lg:hidden', 'xl:block', 'xl:flex', 'xl:grid', 'dark:bg-slate-900', 'dark:bg-slate-950', 'dark:text-white', 'dark:text-slate-300', 'dark:border-slate-700'], 'container',
'box-border',
'box-content',
'overflow-auto',
'overflow-hidden',
'overflow-visible',
'overflow-scroll',
'overflow-x-auto',
'overflow-y-auto',
],
Position: [
'static',
'fixed',
'absolute',
'relative',
'sticky',
'inset-0',
'top-0',
'right-0',
'bottom-0',
'left-0',
'z-0',
'z-10',
'z-20',
'z-30',
'z-40',
'z-50',
],
Flexbox: [
'flex-row',
'flex-row-reverse',
'flex-col',
'flex-col-reverse',
'flex-wrap',
'flex-nowrap',
'grow',
'grow-0',
'shrink',
'shrink-0',
'items-start',
'items-center',
'items-end',
'items-stretch',
'justify-start',
'justify-center',
'justify-end',
'justify-between',
'justify-around',
'justify-evenly',
'self-auto',
'self-start',
'self-center',
'self-end',
'self-stretch',
],
Grid: [
'grid-cols-1',
'grid-cols-2',
'grid-cols-3',
'grid-cols-4',
'grid-cols-5',
'grid-cols-6',
'grid-cols-12',
'col-auto',
'col-span-1',
'col-span-2',
'col-span-3',
'col-span-4',
'col-span-6',
'col-span-12',
'grid-rows-1',
'grid-rows-2',
'grid-rows-3',
'row-span-1',
'row-span-2',
'row-span-3',
],
Spacing: [
'm-0',
'm-1',
'm-2',
'm-3',
'm-4',
'm-5',
'm-6',
'm-8',
'm-auto',
'mx-auto',
'my-auto',
'mt-1',
'mt-2',
'mt-3',
'mt-4',
'mr-1',
'mr-2',
'mr-4',
'mb-1',
'mb-2',
'mb-4',
'ml-1',
'ml-2',
'ml-4',
'p-0',
'p-1',
'p-2',
'p-3',
'p-4',
'p-5',
'p-6',
'p-8',
'px-1',
'px-2',
'px-3',
'px-4',
'px-6',
'py-1',
'py-2',
'py-3',
'py-4',
'py-6',
'gap-0',
'gap-1',
'gap-2',
'gap-3',
'gap-4',
'gap-6',
'gap-8',
],
Sizing: [
'w-0',
'w-auto',
'w-full',
'w-screen',
'w-1/2',
'w-1/3',
'w-2/3',
'w-1/4',
'w-3/4',
'h-0',
'h-auto',
'h-full',
'h-screen',
'min-w-0',
'min-w-full',
'min-h-0',
'min-h-full',
'max-w-xs',
'max-w-sm',
'max-w-md',
'max-w-lg',
'max-w-xl',
'max-w-2xl',
'max-w-full',
'max-h-full',
'aspect-square',
'aspect-video',
],
Typography: [
'text-xs',
'text-sm',
'text-base',
'text-lg',
'text-xl',
'text-2xl',
'text-3xl',
'font-thin',
'font-light',
'font-normal',
'font-medium',
'font-semibold',
'font-bold',
'italic',
'not-italic',
'underline',
'line-through',
'no-underline',
'text-left',
'text-center',
'text-right',
'uppercase',
'lowercase',
'capitalize',
'truncate',
'whitespace-normal',
'whitespace-nowrap',
'break-words',
'leading-none',
'leading-tight',
'leading-normal',
'tracking-tight',
'tracking-normal',
'tracking-wide',
],
Colors: [
'text-transparent',
'text-white',
'text-black',
'text-slate-400',
'text-slate-500',
'text-slate-600',
'text-slate-700',
'text-slate-800',
'text-slate-900',
'text-sky-500',
'text-blue-600',
'text-red-500',
'text-amber-500',
'text-emerald-500',
'bg-transparent',
'bg-white',
'bg-black',
'bg-slate-50',
'bg-slate-100',
'bg-slate-200',
'bg-slate-700',
'bg-slate-800',
'bg-slate-900',
'bg-sky-500',
'bg-blue-600',
'bg-red-500',
'bg-amber-500',
'bg-emerald-500',
],
Border: [
'border',
'border-0',
'border-2',
'border-t',
'border-r',
'border-b',
'border-l',
'border-transparent',
'border-slate-200',
'border-slate-300',
'border-slate-700',
'border-sky-500',
'rounded-none',
'rounded-sm',
'rounded',
'rounded-md',
'rounded-lg',
'rounded-xl',
'rounded-2xl',
'rounded-full',
'divide-x',
'divide-y',
],
Effects: [
'shadow-none',
'shadow-sm',
'shadow',
'shadow-md',
'shadow-lg',
'shadow-xl',
'opacity-0',
'opacity-50',
'opacity-75',
'opacity-100',
'blur-sm',
'blur',
'backdrop-blur-sm',
'backdrop-blur',
'transition',
'transition-all',
'duration-150',
'duration-300',
'ease-in',
'ease-out',
'ease-in-out',
],
Interaction: [
'cursor-auto',
'cursor-default',
'cursor-pointer',
'cursor-not-allowed',
'select-none',
'select-text',
'pointer-events-none',
'pointer-events-auto',
'resize',
'resize-none',
'outline-none',
'focus:outline-none',
'hover:opacity-80',
'hover:bg-slate-100',
'focus:ring-2',
'focus:ring-sky-500',
],
Responsive: [
'sm:block',
'sm:flex',
'sm:grid',
'sm:hidden',
'md:block',
'md:flex',
'md:grid',
'md:hidden',
'lg:block',
'lg:flex',
'lg:grid',
'lg:hidden',
'xl:block',
'xl:flex',
'xl:grid',
'dark:bg-slate-900',
'dark:bg-slate-950',
'dark:text-white',
'dark:text-slate-300',
'dark:border-slate-700',
],
} as const } as const
const splitClasses = (value: string) => value.split(/\s+/).filter(Boolean) const splitClasses = (value: string) => value.split(/\s+/).filter(Boolean)
const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose }: StyleModalProps) => { const StyleModal = ({
isOpen,
mode,
propertyName,
value = '',
onChange,
onClose,
}: StyleModalProps) => {
const { translate } = useLocalization()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [customClass, setCustomClass] = useState('') const [customClass, setCustomClass] = useState('')
const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | 'Tümü'>('Tümü') const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | '__all__'>('__all__')
const selected = useMemo(() => new Set(splitClasses(value)), [value]) const selected = useMemo(() => new Set(splitClasses(value)), [value])
const classes = useMemo(() => { const classes = useMemo(() => {
const source = category === 'Tümü' ? Object.values(STYLE_GROUPS).flat() : STYLE_GROUPS[category] const source =
category === '__all__' ? Object.values(STYLE_GROUPS).flat() : STYLE_GROUPS[category]
const query = search.trim().toLowerCase() const query = search.trim().toLowerCase()
return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query)) return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query))
}, [category, search]) }, [category, search])
@ -56,41 +375,112 @@ const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose
} }
return ( return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4" onMouseDown={onClose}> <div
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900" onMouseDown={(event) => event.stopPropagation()}> className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4"
onMouseDown={onClose}
>
<div
className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900"
onMouseDown={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700"> <div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700">
<div> <div>
<h2 className="font-semibold text-slate-900 dark:text-white">StyleModal · {propertyName}</h2> <h2 className="font-semibold text-slate-900 dark:text-white">
<p className="mt-1 text-xs text-slate-500">{mode === 'css' ? 'CSS kodunu doğrudan düzenleyin.' : 'Hazır sınıfları seçin veya CSS/Tailwind class adlarını elle düzenleyin.'}</p> StyleModal · {propertyName}
</h2>
<p className="mt-1 text-xs text-slate-500">
{translate(
mode === 'css' ? '::App.StyleModal.CssModeHint' : '::App.StyleModal.ClassModeHint',
)}
</p>
</div> </div>
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} /> <Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
</div> </div>
{mode === 'class' && <div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700"> {mode === 'class' && (
<div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
<label className="relative"> <label className="relative">
<FaSearch className="absolute left-3 top-3 text-slate-400" /> <FaSearch className="absolute left-3 top-3 text-slate-400" />
<input className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Hazır sınıflarda ara…" value={search} onChange={(event) => setSearch(event.target.value)} /> <input
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
placeholder={translate('::App.StyleModal.SearchClasses')}
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</label> </label>
<select className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" value={category} onChange={(event) => setCategory(event.target.value as typeof category)}> <select
<option value="Tümü">Tüm kategoriler</option> className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
{Object.keys(STYLE_GROUPS).map((name) => <option key={name} value={name}>{name}</option>)} value={category}
</select> onChange={(event) => setCategory(event.target.value as typeof category)}
</div>} >
{mode === 'class' && <div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700"> <option value="__all__">{translate('::App.StyleModal.AllCategories')}</option>
<input className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Örn. hover:scale-105 w-[320px] [&>span]:font-bold" value={customClass} onChange={(event) => setCustomClass(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addCustomClass() }} /> {Object.keys(STYLE_GROUPS).map((name) => (
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>Ekle</Button> <option key={name} value={name}>
</div>} {name}
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950"> </option>
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">{propertyName} değerini elle düzenle</label>
<textarea className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`} placeholder={mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'} value={value} onChange={(event) => onChange(event.target.value)} />
</div>
{mode === 'class' && <div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
{classes.map((className) => (
<button key={className} className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`} type="button" onClick={() => toggleClass(className)}>{className}</button>
))} ))}
</div>} </select>
</div>
)}
{mode === 'class' && (
<div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700">
<input
className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white"
placeholder={translate('::App.StyleModal.CustomClassPlaceholder')}
value={customClass}
onChange={(event) => setCustomClass(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') addCustomClass()
}}
/>
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>
{translate('::ListForms.Wizard.Add')}
</Button>
</div>
)}
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">
{translate('::App.StyleModal.EditValueManually', { property: propertyName })}
</label>
<textarea
className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`}
placeholder={
mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'
}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</div>
{mode === 'class' && (
<div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
{classes.map((className) => (
<button
key={className}
className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`}
type="button"
onClick={() => toggleClass(className)}
>
{className}
</button>
))}
</div>
)}
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700"> <div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
<span className="text-xs text-slate-500">{mode === 'css' ? 'Ham CSS düzenleme modu' : `${selected.size} seçili · ${classes.length} görüntüleniyor`}</span> <span className="text-xs text-slate-500">
<div className="flex gap-2"><Button size="sm" variant="default" onClick={() => onChange('')}>Temizle</Button><Button size="sm" variant="solid" onClick={onClose}>Tamam</Button></div> {mode === 'css'
? translate('::App.StyleModal.RawCssMode')
: translate('::App.StyleModal.SelectedShownCount', {
selected: selected.size,
shown: classes.length,
})}
</span>
<div className="flex gap-2">
<Button size="sm" variant="default" onClick={() => onChange('')}>
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
</Button>
<Button size="sm" variant="solid" onClick={onClose}>
{translate('::OK')}
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -1,89 +1,80 @@
import React from "react"; import React from 'react'
import classNames from "classnames"; import classNames from 'classnames'
import { import { FaCheckCircle, FaExclamationTriangle, FaClock, FaTimes } from 'react-icons/fa'
FaCheckCircle,
FaExclamationTriangle,
FaClock,
FaTimes,
} from "react-icons/fa";
interface StatusBadgeProps { interface StatusBadgeProps {
status: string; status: string
size?: "sm" | "md"; size?: 'sm' | 'md'
showIcon?: boolean; showIcon?: boolean
} }
const StatusBadge: React.FC<StatusBadgeProps> = ({ const StatusBadge: React.FC<StatusBadgeProps> = ({ status, size = 'md', showIcon = true }) => {
status,
size = "md",
showIcon = true,
}) => {
const sizeClasses = { const sizeClasses = {
sm: "px-1.5 py-0.5 text-xs", sm: 'px-1.5 py-0.5 text-xs',
md: "px-2 py-0.5 text-xs", md: 'px-2 py-0.5 text-xs',
}; }
const getStatusConfig = (status: string) => { const getStatusConfig = (status: string) => {
switch (status) { switch (status) {
case "active": case 'active':
return { return {
color: "bg-green-100 text-green-800", color: 'bg-green-100 text-green-800',
icon: <FaCheckCircle size={14} />, icon: <FaCheckCircle size={14} />,
text: "Aktif", text: 'Aktif',
}; }
case "inactive": case 'inactive':
return { return {
color: "bg-red-100 text-red-800", color: 'bg-red-100 text-red-800',
icon: <FaTimes size={14} />, icon: <FaTimes size={14} />,
text: "Pasif", text: 'Pasif',
}; }
case "pending": case 'pending':
return { return {
color: "bg-yellow-100 text-yellow-800", color: 'bg-yellow-100 text-yellow-800',
icon: <FaClock size={14} />, icon: <FaClock size={14} />,
text: "Beklemede", text: 'Beklemede',
}; }
case "critical": case 'critical':
return { return {
color: "bg-red-100 text-red-800", color: 'bg-red-100 text-red-800',
icon: <FaExclamationTriangle size={14} />, icon: <FaExclamationTriangle size={14} />,
text: "Kritik", text: 'Kritik',
}; }
case "low": case 'low':
return { return {
color: "bg-yellow-100 text-yellow-800", color: 'bg-yellow-100 text-yellow-800',
icon: <FaExclamationTriangle size={14} />, icon: <FaExclamationTriangle size={14} />,
text: "Düşük", text: 'App.Platform.Priority.Low',
}; }
case "normal": case 'normal':
return { return {
color: "bg-green-100 text-green-800", color: 'bg-green-100 text-green-800',
icon: <FaCheckCircle size={14} />, icon: <FaCheckCircle size={14} />,
text: "Normal", text: 'Normal',
}; }
default: default:
return { return {
color: "bg-gray-100 text-gray-800", color: 'bg-gray-100 text-gray-800',
icon: <FaCheckCircle size={14} />, icon: <FaCheckCircle size={14} />,
text: "Bilinmiyor", text: 'Bilinmiyor',
}; }
}
} }
};
const config = getStatusConfig(status); const config = getStatusConfig(status)
return ( return (
<span <span
className={classNames( className={classNames(
"inline-flex items-center rounded-full font-medium", 'inline-flex items-center rounded-full font-medium',
config.color, config.color,
sizeClasses[size] sizeClasses[size],
)} )}
> >
{showIcon && <span className="mr-1">{config.icon}</span>} {showIcon && <span className="mr-1">{config.icon}</span>}
{config.text} {config.text}
</span> </span>
); )
}; }
export default StatusBadge; export default StatusBadge

View file

@ -3,6 +3,7 @@ import DynamicRenderer from './DynamicRenderer'
import { useComponents } from '@/contexts/ComponentContext' import { useComponents } from '@/contexts/ComponentContext'
import { parseComponentDependencies } from '@/contexts/componentRuntime' import { parseComponentDependencies } from '@/contexts/componentRuntime'
import { Loading } from '../shared' import { Loading } from '../shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
export interface ComponentPreviewProps { export interface ComponentPreviewProps {
componentName?: string componentName?: string
@ -10,6 +11,7 @@ export interface ComponentPreviewProps {
} }
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => { const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
const { translate } = useLocalization()
const { components, loading } = useComponents() const { components, loading } = useComponents()
// Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor. // Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor.
@ -19,7 +21,11 @@ const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, clas
}, [components, componentName]) }, [components, componentName])
if (!componentName) { if (!componentName) {
return <div className="text-sm text-gray-500 dark:text-gray-400">Bileşen ismi yok.</div> return (
<div className="text-sm text-gray-500 dark:text-gray-400">
{translate('::App.DeveloperKit.ComponentPreview.NoComponentName')}
</div>
)
} }
if (loading || !Array.isArray(components)) { if (loading || !Array.isArray(components)) {

View file

@ -1,34 +1,34 @@
import React from "react"; import React from 'react'
import type { ErrorInfo } from "react"; import type { ErrorInfo } from 'react'
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode
} }
interface State { interface State {
hasError: boolean; hasError: boolean
error: Error | null; error: Error | null
} }
class ErrorBoundary extends React.Component<Props, State> { class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props)
this.state = { hasError: false, error: null }; this.state = { hasError: false, error: null }
} }
static getDerivedStateFromError(error: Error): State { static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }; return { hasError: true, error }
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
// Eğer component değişmişse, hata state'ini sıfırla // Eğer component değişmişse, hata state'ini sıfırla
if (prevProps.children !== this.props.children && this.state.hasError) { if (prevProps.children !== this.props.children && this.state.hasError) {
this.setState({ hasError: false, error: null }); this.setState({ hasError: false, error: null })
} }
} }
componentDidCatch(error: Error, info: ErrorInfo) { componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Render hatası:", error, info); console.error('Render hatası:', error, info)
} }
render() { render() {
@ -36,27 +36,27 @@ class ErrorBoundary extends React.Component<Props, State> {
return ( return (
<div <div
style={{ style={{
color: "red", color: 'red',
background: "#fff0f0", background: '#fff0f0',
border: "1px solid #f44336", border: '1px solid #f44336',
padding: "1rem", padding: '1rem',
borderRadius: "8px", borderRadius: '8px',
fontFamily: "monospace", fontFamily: 'monospace',
whiteSpace: "pre-wrap", whiteSpace: 'pre-wrap',
}} }}
> >
Render Hatası: Render error:
<br /> <br />
<details open style={{ marginTop: "0.5rem" }}> <details open style={{ marginTop: '0.5rem' }}>
<summary>Detayları Göster</summary> <summary>Show details</summary>
{this.state.error?.message || String(this.state.error)} {this.state.error?.message || String(this.state.error)}
</details> </details>
</div> </div>
); )
} }
return this.props.children; return this.props.children
} }
} }
export default ErrorBoundary; export default ErrorBoundary

View file

@ -41,9 +41,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
const importService = useMemo(() => new ImportService(), []) const importService = useMemo(() => new ImportService(), [])
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set()) const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set())
const [sessionExecutes, setSessionExecutes] = useState< const [sessionExecutes, setSessionExecutes] = useState<Record<string, ListFormImportLogDto[]>>({})
Record<string, ListFormImportLogDto[]>
>({})
const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set()) const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set())
useEffect(() => { useEffect(() => {
@ -414,7 +412,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</thead> </thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800"> <tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{editableColumns.map((column: any) => ( {editableColumns.map((column: any) => (
<tr key={column.fieldName} className="hover:bg-gray-50 dark:hover:bg-gray-800/70"> <tr
key={column.fieldName}
className="hover:bg-gray-50 dark:hover:bg-gray-800/70"
>
<td className="px-2 py-2 font-medium text-gray-800 dark:text-gray-100"> <td className="px-2 py-2 font-medium text-gray-800 dark:text-gray-100">
{column.fieldName} {column.fieldName}
</td> </td>
@ -740,8 +741,14 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
size="xs" size="xs"
> >
<span> <span>
{execute.errorRows} hata detayı {translate('::App.Listforms.ImportManager.ErrorDetails', {
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'} count: execute.errorRows,
})}{' '}
{translate(
expandedErrors.has(execute.id)
? '::App.Platform.Hide'
: '::App.Platform.Show',
)}
</span> </span>
</Button> </Button>
@ -752,10 +759,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<thead className="bg-orange-100 dark:bg-gray-800 sticky top-0"> <thead className="bg-orange-100 dark:bg-gray-800 sticky top-0">
<tr> <tr>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16"> <th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
Satır {translate('::App.Platform.Row')}
</th> </th>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300"> <th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
Hata Mesajı {translate('::App.Platform.ErrorMessage')}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -777,7 +784,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</table> </table>
) : ( ) : (
<p className="px-3 py-2 text-orange-600 dark:text-orange-300"> <p className="px-3 py-2 text-orange-600 dark:text-orange-300">
Hata detayı mevcut değil. {translate(
'::App.Listforms.ImportManager.NoErrorDetail',
)}
</p> </p>
)} )}
</div> </div>

View file

@ -70,8 +70,7 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
{getStatusMessage()} {getStatusMessage()}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
{translate('::App.Listforms.Status.Processing')}{' '} {translate('::App.Listforms.Status.Processing')} {session.blobName}
{session.blobName}
</p> </p>
</div> </div>

View file

@ -38,10 +38,7 @@ const layouts = {
const AuthLayout = lazy(() => import('./AuthLayout')) const AuthLayout = lazy(() => import('./AuthLayout'))
const PublicLayout = lazy(() => import('./PublicLayout')) const PublicLayout = lazy(() => import('./PublicLayout'))
class LayoutChunkErrorBoundary extends Component< class LayoutChunkErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
{ children: ReactNode },
{ failed: boolean }
> {
state = { failed: false } state = { failed: false }
static getDerivedStateFromError() { static getDerivedStateFromError() {
@ -57,16 +54,21 @@ class LayoutChunkErrorBoundary extends Component<
return ( return (
<div className="flex min-h-screen items-center justify-center p-6"> <div className="flex min-h-screen items-center justify-center p-6">
<div className="max-w-md text-center"> <div className="max-w-md text-center">
<h1 className="mb-2 text-lg font-semibold">Uygulama kabuğu yüklenemedi</h1> {/*
Bu sınır, uygulama konfigürasyonu (ve dolayısıyla lokalizasyon metinleri)
yüklenemeden devreye girer; `translate` burada anahtarın kendisini döndürürdü.
Bu yüzden ılış öncesi ekranların tamamı gibi sabit İngilizce tutulur.
*/}
<h1 className="mb-2 text-lg font-semibold">Application shell could not be loaded</h1>
<p className="mb-4 text-sm text-gray-500"> <p className="mb-4 text-sm text-gray-500">
Bağlantınızı kontrol edip uygulamayı yeniden yükleyin. Check your connection and reload the application.
</p> </p>
<button <button
type="button" type="button"
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white" className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white"
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
> >
Yeniden yükle Reload
</button> </button>
</div> </div>
</div> </div>

View file

@ -136,7 +136,7 @@ const PublicLayout = () => {
type="button" type="button"
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)} onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
aria-pressed={!isDarkMode} aria-pressed={!isDarkMode}
title="Light mode" title={translate('::App.Theme.LightMode')}
icon={<LuSun size={15} strokeWidth={1.8} />} icon={<LuSun size={15} strokeWidth={1.8} />}
variant="plain" variant="plain"
size="xs" size="xs"
@ -151,7 +151,7 @@ const PublicLayout = () => {
type="button" type="button"
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)} onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
aria-pressed={isDarkMode} aria-pressed={isDarkMode}
title="Dark mode" title={translate('::App.Theme.DarkMode')}
icon={<LuMoon size={15} strokeWidth={1.8} />} icon={<LuMoon size={15} strokeWidth={1.8} />}
variant="plain" variant="plain"
size="xs" size="xs"
@ -199,7 +199,11 @@ const PublicLayout = () => {
'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group' 'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group'
const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white' const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white'
return link.path ? ( return link.path ? (
<Link key={link.path} to={link.path} className={`${baseClass} ${activeClass}`}> <Link
key={link.path}
to={link.path}
className={`${baseClass} ${activeClass}`}
>
{link.icon && ( {link.icon && (
<link.icon <link.icon
size={22} size={22}
@ -270,14 +274,10 @@ const PublicLayout = () => {
<Button <Button
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors" className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
onClick={toggleMenu} onClick={toggleMenu}
aria-label="Toggle menu" aria-label={translate('::App.Platform.ToggleMenu')}
aria-expanded={isOpen} aria-expanded={isOpen}
icon={ icon={
isOpen ? ( isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />
<LuX size={20} strokeWidth={2} />
) : (
<LuMenu size={20} strokeWidth={2} />
)
} }
variant="plain" variant="plain"
size="xs" size="xs"
@ -545,7 +545,9 @@ const PublicLayout = () => {
{/* Contact Info */} {/* Contact Info */}
<div> <div>
<h3 className="text-lg font-bold mb-4 text-white">{translate('::App.Contact')}</h3> <h3 className="text-lg font-bold mb-4 text-white">
{translate('::App.Contact')}
</h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-start space-x-3"> <li className="flex items-start space-x-3">
<LuMapPin <LuMapPin
@ -556,7 +558,11 @@ const PublicLayout = () => {
<span className="text-gray-400">{translate('::Public.footer.address')}</span> <span className="text-gray-400">{translate('::Public.footer.address')}</span>
</li> </li>
<li className="flex items-center space-x-3"> <li className="flex items-center space-x-3">
<LuPhone size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" /> <LuPhone
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a <a
href="tel:+905447697638" href="tel:+905447697638"
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors"
@ -565,7 +571,11 @@ const PublicLayout = () => {
</a> </a>
</li> </li>
<li className="flex items-center space-x-3"> <li className="flex items-center space-x-3">
<LuMail size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" /> <LuMail
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a <a
href="mailto:destek@sozsoft.com" href="mailto:destek@sozsoft.com"
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors"

View file

@ -35,7 +35,7 @@ export const Basket: React.FC<BasketProps> = ({
const { translate } = useLocalization() const { translate } = useLocalization()
const handleClearBasket = () => { const handleClearBasket = () => {
if (window.confirm('Sepetteki tüm ürünleri silmek istediğinizden emin misiniz?')) { if (window.confirm(translate('::Public.basket.clearConfirm'))) {
clearBasket() clearBasket()
} }
} }

View file

@ -53,9 +53,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'} variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
size="xs" size="xs"
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''} className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
title={ title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
}
> >
{translate('::Public.products.billingcycle.monthly')} {translate('::Public.products.billingcycle.monthly')}
</Button> </Button>
@ -65,9 +63,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'} variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
size="xs" size="xs"
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''} className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
title={ title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
}
> >
{translate('::Public.products.billingcycle.yearly')} {translate('::Public.products.billingcycle.yearly')}
</Button> </Button>
@ -90,11 +86,13 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
icon={<FaMinus className="w-4 h-4" />} icon={<FaMinus className="w-4 h-4" />}
variant="default" variant="default"
size="xs" size="xs"
title={hasBasketItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined} title={hasBasketItems ? translate('::Public.basket.periodLocked') : undefined}
/> />
<div className="flex items-center space-x-2 bg-gray-50 px-2 py-1 rounded-lg dark:bg-gray-800"> <div className="flex items-center space-x-2 bg-gray-50 px-2 py-1 rounded-lg dark:bg-gray-800">
<span className="font-bold text-lg text-gray-600 dark:text-white">{globalPeriod}</span> <span className="font-bold text-lg text-gray-600 dark:text-white">
{globalPeriod}
</span>
<span className="text-xs text-gray-600 dark:text-white"> <span className="text-xs text-gray-600 dark:text-white">
{globalBillingCycle === 'monthly' {globalBillingCycle === 'monthly'
? translate('::Public.products.billingcycle.month') ? translate('::Public.products.billingcycle.month')
@ -108,7 +106,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
icon={<FaPlus className="w-4 h-4" />} icon={<FaPlus className="w-4 h-4" />}
variant="default" variant="default"
size="xs" size="xs"
title={hasBasketItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined} title={hasBasketItems ? translate('::Public.basket.periodLocked') : undefined}
/> />
</div> </div>
</div> </div>

View file

@ -1,5 +1,12 @@
import React from 'react' import React from 'react'
import { FaArrowLeft, FaBuilding, FaCalendarAlt, FaCheckCircle, FaEnvelope, FaPhone } from 'react-icons/fa' import {
FaArrowLeft,
FaBuilding,
FaCalendarAlt,
FaCheckCircle,
FaEnvelope,
FaPhone,
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
@ -42,8 +49,8 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
const getItemPrice = (item: BasketItem) => const getItemPrice = (item: BasketItem) =>
item.billingCycle === 'monthly' item.billingCycle === 'monthly'
? item.product.monthlyPrice ?? 0 ? (item.product.monthlyPrice ?? 0)
: item.product.yearlyPrice ?? 0 : (item.product.yearlyPrice ?? 0)
const vatRateLabel = (() => { const vatRateLabel = (() => {
if (!order) return '' if (!order) return ''
@ -117,7 +124,9 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
</h3> </h3>
<div className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300 sm:grid-cols-2">
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70"> <div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
<div className="text-xs text-gray-500 dark:text-gray-400">License Start Time</div> <div className="text-xs text-gray-500 dark:text-gray-400">
License Start Time
</div>
<div className="font-semibold">{formatDate(order.licenseStartTime)}</div> <div className="font-semibold">{formatDate(order.licenseStartTime)}</div>
</div> </div>
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70"> <div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
@ -158,7 +167,11 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
<div className="col-span-1 text-right">{order.period}</div> <div className="col-span-1 text-right">{order.period}</div>
<div className="col-span-1 text-right">{item.quantity}</div> <div className="col-span-1 text-right">{item.quantity}</div>
<div className="col-span-1 text-right"> <div className="col-span-1 text-right">
{item.billingCycle === 'monthly' ? 'Aylık' : 'Yıllık'} {translate(
item.billingCycle === 'monthly'
? '::Public.order.monthly'
: '::Public.order.yearly',
)}
</div> </div>
<div className="col-span-2 text-right"> <div className="col-span-2 text-right">
%{((item.vatRate ?? 0) * 100).toFixed(0)} / {formatPrice(item.vatAmount ?? 0)} %{((item.vatRate ?? 0) * 100).toFixed(0)} / {formatPrice(item.vatAmount ?? 0)}
@ -177,7 +190,10 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
<span>{formatPrice(order.subtotal)}</span> <span>{formatPrice(order.subtotal)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span> <span>
{translate('::Public.products.kdv')}
{vatRateLabel}
</span>
<span>{formatPrice(order.vatTotal)}</span> <span>{formatPrice(order.vatTotal)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">

View file

@ -28,7 +28,7 @@ interface PaymentFormProps {
} }
export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, basketData }) => { export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, basketData }) => {
const defaultPaymentMethod = 'Kredi Kartı' const defaultPaymentMethod = 'CreditCard'
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod) const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod)
const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>() const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>()
const [paymentData, setPaymentData] = useState({ const [paymentData, setPaymentData] = useState({
@ -390,7 +390,10 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, ba
<span>{formatPrice(basketData.subtotal)}</span> <span>{formatPrice(basketData.subtotal)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span> <span>
{translate('::Public.products.kdv')}
{vatRateLabel}
</span>
<span>{formatPrice(basketData.vatTotal)}</span> <span>{formatPrice(basketData.vatTotal)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">

View file

@ -114,9 +114,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
</span> </span>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 dark:text-gray-100">{translate('::' + product.name)}</h3> <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 dark:text-gray-100">
{translate('::' + product.name)}
</h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">{translate('::' + product.description)}</p> <p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">
{translate('::' + product.description)}
</p>
{/* Quantity and Yearly Savings above price */} {/* Quantity and Yearly Savings above price */}
<div className="mb-4 space-y-3"> <div className="mb-4 space-y-3">
@ -157,15 +161,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
<div className="mb-4"> <div className="mb-4">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{formatPrice(getCurrentPrice())} {formatPrice(getCurrentPrice())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getUnitText()}</span> <span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
{getUnitText()}
</span>
</div> </div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{translate('::Public.products.kdv')} (%{(getVatRate() * 100).toFixed(0)}): {formatPrice(getVatAmount())} {translate('::Public.products.kdv')} (%{(getVatRate() * 100).toFixed(0)}):{' '}
{formatPrice(getVatAmount())}
</div> </div>
{globalPeriod > 1 && ( {globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1"> <div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPriceWithVat())} {translate('::App.Listform.ListformField.Total')}{' '}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getPeriodText()}</span> {formatPrice(getTotalPriceWithVat())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
{getPeriodText()}
</span>
</div> </div>
)} )}
{globalPeriod <= 1 && ( {globalPeriod <= 1 && (
@ -188,7 +198,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
size="md" size="md"
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''} className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
> >
{isDisabled ? translate('::Public.products.inBasket') : translate('::Public.products.addToBasket')} {isDisabled
? translate('::Public.products.inBasket')
: translate('::Public.products.addToBasket')}
</Button> </Button>
) )
})()} })()}

View file

@ -91,8 +91,12 @@ const getNationalPhoneDigits = (value: string, country?: CountryDto) => {
return digits return digits
} }
const getPhoneValidationError = (value: string, country?: CountryDto) => { const getPhoneValidationError = (
if (!country) return 'Lütfen önce ülke seçin.' value: string,
country: CountryDto | undefined,
translate: (key: string, params?: Record<string, string | number>) => string,
) => {
if (!country) return translate('::App.Tenant.SelectCountryFirst')
const length = getNationalPhoneDigits(value, country).length const length = getNationalPhoneDigits(value, country).length
const minLength = getPhoneMinLength(country) const minLength = getPhoneMinLength(country)
@ -100,8 +104,8 @@ const getPhoneValidationError = (value: string, country?: CountryDto) => {
if (length >= minLength && length <= maxLength) return '' if (length >= minLength && length <= maxLength) return ''
return minLength === maxLength return minLength === maxLength
? `Telefon numarası ${minLength} haneli olmalıdır.` ? translate('::App.Tenant.PhoneExactLength', { length: minLength })
: `Telefon numarası ${minLength}-${maxLength} hane arasında olmalıdır.` : translate('::App.Tenant.PhoneRangeLength', { min: minLength, max: maxLength })
} }
const formatPhoneNumber = (value: string, country?: CountryDto) => { const formatPhoneNumber = (value: string, country?: CountryDto) => {
@ -309,7 +313,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
if (isExisting) { if (isExisting) {
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
setExistingTenantError('Lütfen önce organization code girin.') setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return return
} }
@ -318,12 +322,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
normalizeCode(foundTenantName) !== normalizeCode(formData.name) || normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
!formData.organizationName !formData.organizationName
) { ) {
setExistingTenantError('Devam etmek için önce organization code ile kurum bilgisini bulun.') setExistingTenantError(translate('::App.Tenant.FindOrgBeforeContinue'))
return return
} }
if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) { if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) {
setExistingTenantError('Kurum bilgileri eksik geldi. Lütfen kurum kodunu tekrar aratın.') setExistingTenantError(translate('::App.Tenant.OrgDataIncomplete'))
return return
} }
@ -338,23 +342,27 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
} }
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) { if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
setFormError('Lütfen zorunlu alanları eksiksiz doldurun.') setFormError(translate('::App.Tenant.FillRequiredFields'))
hasValidationError = true hasValidationError = true
} }
const nextPhoneError = getPhoneValidationError(formData.phoneNumber || '', selectedCountry) const nextPhoneError = getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
)
setPhoneError(nextPhoneError) setPhoneError(nextPhoneError)
if (nextPhoneError) { if (nextPhoneError) {
hasValidationError = true hasValidationError = true
} }
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) { if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
setEmailError('Lütfen geçerli bir e-posta adresi girin.') setEmailError(translate('::App.Platform.InvalidEmail'))
hasValidationError = true hasValidationError = true
} }
if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) { if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) {
setWebsiteError('Lütfen geçerli bir web sitesi adresi girin.') setWebsiteError(translate('::App.Tenant.InvalidWebsite'))
hasValidationError = true hasValidationError = true
} }
@ -499,7 +507,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const getTenantInfo = async () => { const getTenantInfo = async () => {
const tenantName = formData.name?.trim() const tenantName = formData.name?.trim()
if (!tenantName) { if (!tenantName) {
setExistingTenantError('Lütfen önce organization code girin.') setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return return
} }
@ -535,7 +543,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
if (tenant.data.name) setFoundTenantName(tenant.data.name) if (tenant.data.name) setFoundTenantName(tenant.data.name)
} catch (error) { } catch (error) {
setFoundTenantName('') setFoundTenantName('')
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.') setExistingTenantError(translate('::App.Tenant.OrgNotFound'))
console.error('Kurum bilgisi alınırken hata:', error) console.error('Kurum bilgisi alınırken hata:', error)
} finally { } finally {
setIsLoadingTenant(false) setIsLoadingTenant(false)
@ -634,7 +642,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2" className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2"
> >
{isLoadingTenant {isLoadingTenant
? 'Aranıyor...' ? translate('::App.Tenant.Searching')
: translate('::Public.products.tenantForm.searchOrg')} : translate('::Public.products.tenantForm.searchOrg')}
</Button> </Button>
</div> </div>
@ -647,7 +655,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
!canSubmitExistingTenant && !canSubmitExistingTenant &&
!isLoadingTenant && ( !isLoadingTenant && (
<p className="mt-2 text-sm text-amber-600"> <p className="mt-2 text-sm text-amber-600">
Devam etmek için önce Find Organization ile kurum bilgilerini getirin. {translate('::App.Tenant.FindOrgHint')}
</p> </p>
)} )}
@ -807,7 +815,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
countryOptions.find((option) => option.value === formData.country) || null countryOptions.find((option) => option.value === formData.country) || null
} }
onChange={handleCountryChange} onChange={handleCountryChange}
placeholder="Ülke seçin" placeholder={translate('::App.Tenant.SelectCountry')}
/> />
</div> </div>
</div> </div>
@ -827,7 +835,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
options={cityOptions} options={cityOptions}
value={cityOptions.find((option) => option.value === formData.city) || null} value={cityOptions.find((option) => option.value === formData.city) || null}
onChange={handleCityChange} onChange={handleCityChange}
placeholder={formData.country ? 'Şehir seçin' : 'Önce ülke seçin'} placeholder={translate(
formData.country
? '::App.Tenant.SelectCity'
: '::App.Tenant.SelectCountryFirstShort',
)}
/> />
</div> </div>
</div> </div>
@ -847,7 +859,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
null null
} }
onChange={handleDistrictChange} onChange={handleDistrictChange}
placeholder={formData.city ? 'İlçe seçin' : 'Önce şehir seçin'} placeholder={translate(
formData.city
? '::App.Tenant.SelectDistrict'
: '::App.Tenant.SelectCityFirst',
)}
/> />
</div> </div>
</div> </div>
@ -870,7 +886,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
) || null ) || null
} }
onChange={handleTownshipChange} onChange={handleTownshipChange}
placeholder={formData.district ? 'Township seçin' : 'Önce ilçe seçin'} placeholder={translate(
formData.district
? '::App.Tenant.SelectTownship'
: '::App.Tenant.SelectDistrictFirst',
)}
/> />
</div> </div>
</div> </div>
@ -922,7 +942,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
required required
inputMode="numeric" inputMode="numeric"
placeholder={phoneMask.replace(/0/g, '_')} placeholder={phoneMask.replace(/0/g, '_')}
aria-label={`Telefon numarası, format: ${phoneMask}`} aria-label={translate('::App.Tenant.PhoneAria', { mask: phoneMask })}
value={formData.phoneNumber || ''} value={formData.phoneNumber || ''}
onChange={(e) => { onChange={(e) => {
handleInputChange( handleInputChange(
@ -933,7 +953,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
}} }}
onBlur={() => onBlur={() =>
setPhoneError( setPhoneError(
getPhoneValidationError(formData.phoneNumber || '', selectedCountry), getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
),
) )
} }
aria-invalid={Boolean(phoneError)} aria-invalid={Boolean(phoneError)}

View file

@ -1,4 +1,5 @@
import { Button, Dialog } from '@/components/ui' import { Button, Dialog } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Editor, { type Monaco } from '@monaco-editor/react' import Editor, { type Monaco } from '@monaco-editor/react'
import type * as monacoApi from 'monaco-editor' import type * as monacoApi from 'monaco-editor'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -69,9 +70,12 @@ function ScriptBuilderDialog({
dialect, dialect,
onClose, onClose,
onApply, onApply,
saveLabel = 'Kaydet', saveLabel,
cancelLabel = 'Vazgeç', cancelLabel,
}: ScriptBuilderDialogProps) { }: ScriptBuilderDialogProps) {
const { translate } = useLocalization()
const saveText = saveLabel ?? translate('::Save')
const cancelText = cancelLabel ?? translate('::Cancel')
const [rules, setRules] = useState<ScriptRule[]>([]) const [rules, setRules] = useState<ScriptRule[]>([])
const [code, setCode] = useState('') const [code, setCode] = useState('')
// Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür. // Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür.
@ -169,7 +173,11 @@ function ScriptBuilderDialog({
* Ek koşul kalmadığında `conditions` alanı tamamen kaldırılır eski tek * Ek koşul kalmadığında `conditions` alanı tamamen kaldırılır eski tek
* koşullu kurallarla aynı JSON'a serileşmesi buna bağlı. * koşullu kurallarla aynı JSON'a serileşmesi buna bağlı.
*/ */
const updateCondition = (rule: ScriptRule, index: number, patch: Partial<ScriptRuleCondition>) => { const updateCondition = (
rule: ScriptRule,
index: number,
patch: Partial<ScriptRuleCondition>,
) => {
if (index === 0) { if (index === 0) {
updateRule(rule.id, { condition: { ...rule.condition, ...patch } }) updateRule(rule.id, { condition: { ...rule.condition, ...patch } })
return return
@ -191,7 +199,10 @@ function ScriptBuilderDialog({
const removeCondition = (rule: ScriptRule, index: number) => { const removeCondition = (rule: ScriptRule, index: number) => {
const conditions = (rule.conditions ?? []).filter((_, item) => item !== index - 1) const conditions = (rule.conditions ?? []).filter((_, item) => item !== index - 1)
updateRule(rule.id, conditions.length ? { conditions } : { conditions: undefined, join: undefined }) updateRule(
rule.id,
conditions.length ? { conditions } : { conditions: undefined, join: undefined },
)
} }
const moveRule = (index: number, delta: number) => const moveRule = (index: number, delta: number) =>
@ -314,7 +325,7 @@ function ScriptBuilderDialog({
updateRule(rule.id, { fields: [...selectedFields, next] }) updateRule(rule.id, { fields: [...selectedFields, next] })
}} }}
> >
<option value="">+ ekle</option> <option value="">{translate('::App.ScriptBuilder.AddOption')}</option>
{options {options
.filter((fieldName) => !selectedFields.includes(fieldName)) .filter((fieldName) => !selectedFields.includes(fieldName))
.map((fieldName) => ( .map((fieldName) => (
@ -341,7 +352,7 @@ function ScriptBuilderDialog({
param.optionSource, param.optionSource,
currentValue, currentValue,
(next) => updateParam(rule.id, param.key, next), (next) => updateParam(rule.id, param.key, next),
param.placeholder || 'Seç', param.placeholder || translate('::App.ScriptBuilder.Choose'),
)} )}
{param.type === 'select' && ( {param.type === 'select' && (
<select <select
@ -384,7 +395,9 @@ function ScriptBuilderDialog({
<div key={index} className="grid grid-cols-12 items-end gap-2"> <div key={index} className="grid grid-cols-12 items-end gap-2">
{index > 0 ? ( {index > 0 ? (
<label className="col-span-12 min-w-0 md:col-span-2"> <label className="col-span-12 min-w-0 md:col-span-2">
<span className="mb-1 block text-xs text-gray-500">Bağlaç</span> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Conjunction')}
</span>
<select <select
className={controlClass} className={controlClass}
value={rule.join ?? 'and'} value={rule.join ?? 'and'}
@ -392,19 +405,25 @@ function ScriptBuilderDialog({
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] }) updateRule(rule.id, { join: event.target.value as ScriptRule['join'] })
} }
> >
<option value="and">VE</option> <option value="and">{translate('::App.ScriptBuilder.And')}</option>
<option value="or">VEYA</option> <option value="or">{translate('::App.ScriptBuilder.Or')}</option>
</select> </select>
</label> </label>
) : ( ) : (
<div className="col-span-12 hidden md:col-span-2 md:block"> <div className="col-span-12 hidden md:col-span-2 md:block">
<span className="mb-1 block text-xs text-gray-500">Koşul</span> <span className="mb-1 block text-xs text-gray-500">
<div className="flex h-9 items-center text-xs font-semibold text-gray-400">EĞER</div> {translate('::App.ScriptBuilder.Condition')}
</span>
<div className="flex h-9 items-center text-xs font-semibold text-gray-400">
{translate('::App.ScriptBuilder.If')}
</div>
</div> </div>
)} )}
<label className="col-span-12 min-w-0 md:col-span-3"> <label className="col-span-12 min-w-0 md:col-span-3">
<span className="mb-1 block text-xs text-gray-500">Karşılaştırma</span> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Comparison')}
</span>
<select <select
className={controlClass} className={controlClass}
value={condition.operator} value={condition.operator}
@ -412,7 +431,7 @@ function ScriptBuilderDialog({
> >
{dialect.operators.map((item) => ( {dialect.operators.map((item) => (
<option key={item.value} value={item.value}> <option key={item.value} value={item.value}>
{item.label} {item.label.startsWith('App.') ? translate('::' + item.label) : item.label}
</option> </option>
))} ))}
</select> </select>
@ -420,7 +439,9 @@ function ScriptBuilderDialog({
{operator?.needsSource && dialect.conditionKinds.length > 1 && ( {operator?.needsSource && dialect.conditionKinds.length > 1 && (
<label className="col-span-12 min-w-0 md:col-span-2"> <label className="col-span-12 min-w-0 md:col-span-2">
<span className="mb-1 block text-xs text-gray-500">Kaynak</span> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Source')}
</span>
<select <select
className={controlClass} className={controlClass}
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''} value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''}
@ -439,19 +460,23 @@ function ScriptBuilderDialog({
{operator?.needsSource && ( {operator?.needsSource && (
<label className="col-span-12 min-w-0 md:col-span-3"> <label className="col-span-12 min-w-0 md:col-span-3">
<span className="mb-1 block text-xs text-gray-500">Alan</span> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Field')}
</span>
{renderOptionInput( {renderOptionInput(
kind?.optionSource, kind?.optionSource,
condition.source, condition.source,
(next) => updateCondition(rule, index, { source: next }), (next) => updateCondition(rule, index, { source: next }),
kind?.placeholder || 'Seç', kind?.placeholder || translate('::App.ScriptBuilder.Choose'),
)} )}
</label> </label>
)} )}
{operator?.needsValue && ( {operator?.needsValue && (
<label className="col-span-12 min-w-0 md:col-span-2"> <label className="col-span-12 min-w-0 md:col-span-2">
<span className="mb-1 block text-xs text-gray-500">Değer</span> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Value')}
</span>
<input <input
className={controlClass} className={controlClass}
value={condition.value} value={condition.value}
@ -468,7 +493,7 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
variant="plain" variant="plain"
title="Koşulu kaldır" title={translate('::App.ScriptBuilder.RemoveCondition')}
icon={<FaTimes />} icon={<FaTimes />}
onClick={() => removeCondition(rule, index)} onClick={() => removeCondition(rule, index)}
/> />
@ -511,7 +536,8 @@ function ScriptBuilderDialog({
</div> </div>
{conditions.length > 1 && ( {conditions.length > 1 && (
<span className="shrink-0 rounded-full bg-violet-100 px-2 py-[1px] text-[11px] text-violet-700 dark:bg-violet-900 dark:text-violet-200"> <span className="shrink-0 rounded-full bg-violet-100 px-2 py-[1px] text-[11px] text-violet-700 dark:bg-violet-900 dark:text-violet-200">
{conditions.length} koşul · {rule.join === 'or' ? 'VEYA' : 'VE'} {translate('::App.ScriptBuilder.ConditionCount', { count: conditions.length })} ·{' '}
{translate(rule.join === 'or' ? '::App.ScriptBuilder.Or' : '::App.ScriptBuilder.And')}
</span> </span>
)} )}
{triggerInfo && trigger !== dialect.triggers?.[0]?.value && ( {triggerInfo && trigger !== dialect.triggers?.[0]?.value && (
@ -527,10 +553,10 @@ function ScriptBuilderDialog({
className="flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-[1px] text-[11px] text-amber-700 dark:bg-amber-900 dark:text-amber-200" className="flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-[1px] text-[11px] text-amber-700 dark:bg-amber-900 dark:text-amber-200"
title={ title={
dialect.getRuleWarning?.(rule) || dialect.getRuleWarning?.(rule) ||
'Zorunlu alanlar dolmadığı için bu kural scripte eklenmez.' translate('::App.ScriptBuilder.IncompleteRuleWarning')
} }
> >
<FaExclamationTriangle size={9} /> eksik <FaExclamationTriangle size={9} /> {translate('::App.ScriptBuilder.Missing')}
</span> </span>
)} )}
<div className="flex shrink-0 items-center"> <div className="flex shrink-0 items-center">
@ -539,7 +565,7 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
variant="plain" variant="plain"
title="Yukarı taşı" title={translate('::App.Platform.MoveUp')}
icon={<FaArrowUp />} icon={<FaArrowUp />}
onClick={() => moveRule(index, -1)} onClick={() => moveRule(index, -1)}
/> />
@ -548,7 +574,7 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
variant="plain" variant="plain"
title="Aşağı taşı" title={translate('::App.Platform.MoveDown')}
icon={<FaArrowDown />} icon={<FaArrowDown />}
onClick={() => moveRule(index, 1)} onClick={() => moveRule(index, 1)}
/> />
@ -557,7 +583,7 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
variant="plain" variant="plain"
title="Kuralı sil" title={translate('::App.ScriptBuilder.DeleteRule')}
icon={<FaTrash />} icon={<FaTrash />}
onClick={() => touchRules((current) => current.filter((item) => item.id !== rule.id))} onClick={() => touchRules((current) => current.filter((item) => item.id !== rule.id))}
/> />
@ -595,10 +621,10 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
icon={<FaPlus />} icon={<FaPlus />}
title="Aynı kurala ikinci bir koşul ekler; VE/VEYA ile birleşir." title={translate('::App.ScriptBuilder.AddConditionHint')}
onClick={() => addCondition(rule)} onClick={() => addCondition(rule)}
> >
Koşul ekle {translate('::App.ScriptBuilder.AddCondition')}
</Button> </Button>
</div> </div>
</div> </div>
@ -622,7 +648,7 @@ function ScriptBuilderDialog({
<FaSearch className="text-gray-400" size={11} /> <FaSearch className="text-gray-400" size={11} />
<input <input
className="w-full min-w-0 bg-transparent text-xs text-gray-700 outline-none dark:text-gray-100" className="w-full min-w-0 bg-transparent text-xs text-gray-700 outline-none dark:text-gray-100"
placeholder="Kısayol veya snippet ara" placeholder={translate('::App.ScriptBuilder.SearchHelp')}
value={helpSearch} value={helpSearch}
onChange={(event) => setHelpSearch(event.target.value)} onChange={(event) => setHelpSearch(event.target.value)}
/> />
@ -631,7 +657,7 @@ function ScriptBuilderDialog({
size="xs" size="xs"
type="button" type="button"
variant="plain" variant="plain"
title="Paneli kapat" title={translate('::App.Platform.ClosePanel')}
icon={<FaTimes />} icon={<FaTimes />}
onClick={() => setHelpOpen(false)} onClick={() => setHelpOpen(false)}
/> />
@ -656,7 +682,9 @@ function ScriptBuilderDialog({
</button> </button>
))} ))}
{!filteredSnippets.length && ( {!filteredSnippets.length && (
<span className="text-[11px] text-gray-400">Eşleşen snippet yok.</span> <span className="text-[11px] text-gray-400">
{translate('::App.ScriptBuilder.NoMatchingSnippet')}
</span>
)} )}
</div> </div>
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-gray-400"> <div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
@ -678,7 +706,9 @@ function ScriptBuilderDialog({
</button> </button>
))} ))}
{!filteredApi.length && ( {!filteredApi.length && (
<span className="text-[11px] text-gray-400">Eşleşen kısayol yok.</span> <span className="text-[11px] text-gray-400">
{translate('::App.ScriptBuilder.NoMatchingShortcut')}
</span>
)} )}
</div> </div>
</div> </div>
@ -728,15 +758,15 @@ function ScriptBuilderDialog({
<Button <Button
size="sm" size="sm"
type="button" type="button"
title="Kısayol ve snippet panelini aç" title={translate('::App.ScriptBuilder.OpenHelpPanel')}
icon={<FaBook />} icon={<FaBook />}
onClick={() => setHelpOpen(true)} onClick={() => setHelpOpen(true)}
> >
Yardım {translate('::App.ScriptBuilder.Help')}
</Button> </Button>
)} )}
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyScript}> <Button size="sm" type="button" icon={<FaCopy />} onClick={copyScript}>
{copied ? 'Kopyalandı' : 'Kopyala'} {translate(copied ? '::App.Platform.Copied' : '::App.SqlQueryManager.Copy')}
</Button> </Button>
<div className="flex overflow-hidden rounded-md border border-gray-200 dark:border-gray-700"> <div className="flex overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
<button <button
@ -757,10 +787,10 @@ function ScriptBuilderDialog({
? 'bg-indigo-500 text-white' ? 'bg-indigo-500 text-white'
: 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800' : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
}`} }`}
title="Kod yazmadan kural kurmak için sihirbazı aç" title={translate('::App.ScriptBuilder.OpenWizardHint')}
onClick={openWizard} onClick={openWizard}
> >
<FaMagic /> Sihirbaz <FaMagic /> {translate('::App.ScriptBuilder.Wizard')}
</button> </button>
</div> </div>
</div> </div>
@ -769,12 +799,9 @@ function ScriptBuilderDialog({
{view === 'wizard' && codeDiverged && ( {view === 'wizard' && codeDiverged && (
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200"> <div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
<FaExclamationTriangle className="shrink-0" /> <FaExclamationTriangle className="shrink-0" />
<span className="flex-1"> <span className="flex-1">{translate('::App.ScriptBuilder.CodeDivergedWarning')}</span>
Koddaki script kurallardan farklı. Burada bir kuralı değiştirdiğin anda kod
kurallardan yeniden üretilir.
</span>
<Button size="xs" type="button" onClick={() => setView('code')}> <Button size="xs" type="button" onClick={() => setView('code')}>
Kodu gör {translate('::App.ScriptBuilder.ShowCode')}
</Button> </Button>
</div> </div>
)} )}
@ -789,11 +816,11 @@ function ScriptBuilderDialog({
disabled={!generatedScript} disabled={!generatedScript}
size="xs" size="xs"
type="button" type="button"
title="Sihirbazdaki kurallardan script'i yeniden üretir." title={translate('::App.ScriptBuilder.RegenerateHint')}
icon={<FaMagic />} icon={<FaMagic />}
onClick={() => setCode(generatedScript)} onClick={() => setCode(generatedScript)}
> >
Kurallardan üret {translate('::App.ScriptBuilder.Regenerate')}
</Button> </Button>
)} )}
</div> </div>
@ -824,7 +851,9 @@ function ScriptBuilderDialog({
{paletteOpen ? <FaChevronDown size={11} /> : <FaChevronRight size={11} />} {paletteOpen ? <FaChevronDown size={11} /> : <FaChevronRight size={11} />}
</span> </span>
<FaPlus className="text-indigo-500" size={12} /> <FaPlus className="text-indigo-500" size={12} />
<span className="flex-1 text-sm font-semibold">Kural Ekle</span> <span className="flex-1 text-sm font-semibold">
{translate('::App.ScriptBuilder.AddRule')}
</span>
</button> </button>
{paletteOpen && ( {paletteOpen && (
<div className="flex flex-col gap-3 border-t border-gray-100 px-3 py-3 dark:border-gray-800"> <div className="flex flex-col gap-3 border-t border-gray-100 px-3 py-3 dark:border-gray-800">
@ -848,7 +877,7 @@ function ScriptBuilderDialog({
key={recipe.id} key={recipe.id}
size="sm" size="sm"
type="button" type="button"
title={`${recipe.summary}\n\nÖrnek: ${recipe.example}`} title={`${recipe.summary}\n\n${translate('::App.ScriptBuilder.Example')}: ${recipe.example}`}
onClick={() => addRule(recipe.id)} onClick={() => addRule(recipe.id)}
> >
{recipe.label} {recipe.label}
@ -866,8 +895,7 @@ function ScriptBuilderDialog({
{!rules.length && ( {!rules.length && (
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700"> <div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
Henüz kural yok. Yukarıdan bir kural ekle; her kural tek satırlık script üretir ve {translate('::App.ScriptBuilder.NoRulesYet')}
kod sekmesine anında yansır.
</div> </div>
)} )}
</section> </section>
@ -876,10 +904,14 @@ function ScriptBuilderDialog({
<div className="mb-3 flex items-center justify-between gap-2"> <div className="mb-3 flex items-center justify-between gap-2">
<span className="flex items-center gap-2 text-sm font-semibold"> <span className="flex items-center gap-2 text-sm font-semibold">
<FaCode className="text-gray-400" /> <FaCode className="text-gray-400" />
Script Önizleme {translate('::App.ScriptBuilder.ScriptPreview')}
</span> </span>
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] text-gray-500 dark:bg-gray-800 dark:text-gray-300"> <span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] text-gray-500 dark:bg-gray-800 dark:text-gray-300">
{generatedScript ? `${generatedScript.split('\n').length - 1} satır` : 'boş'} {generatedScript
? translate('::App.ScriptBuilder.LineCount', {
count: generatedScript.split('\n').length - 1,
})
: translate('::ListForms.EditorOptions.Empty')}
</span> </span>
</div> </div>
<div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"> <div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
@ -887,13 +919,16 @@ function ScriptBuilderDialog({
height="100%" height="100%"
language="javascript" language="javascript"
theme="vs-dark" theme="vs-dark"
value={generatedScript || '// Kural ekledikçe script burada oluşur'} value={
generatedScript || `// ${translate('::App.ScriptBuilder.EmptyScriptHint')}`
}
options={{ ...editorOptions, readOnly: true }} options={{ ...editorOptions, readOnly: true }}
/> />
</div> </div>
<p className="mt-2 text-[11px] text-gray-400"> <p className="mt-2 text-[11px] text-gray-400">
İlk satırdaki <code>{'// @builder'}</code> yorumu kuralları saklar; dialog yeniden {translate('::App.ScriptBuilder.BuilderCommentHintPrefix')}{' '}
ıldığında sihirbaz kuralları buradan geri yükler. <code>{'// @builder'}</code>{' '}
{translate('::App.ScriptBuilder.BuilderCommentHintSuffix')}
</p> </p>
</section> </section>
</div> </div>
@ -902,7 +937,7 @@ function ScriptBuilderDialog({
<Dialog.Footer className="mt-1 flex justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700"> <Dialog.Footer className="mt-1 flex justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
<Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}> <Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}>
{cancelLabel} {cancelText}
</Button> </Button>
<Button <Button
size="sm" size="sm"
@ -914,7 +949,7 @@ function ScriptBuilderDialog({
onClose() onClose()
}} }}
> >
{saveLabel} {saveText}
</Button> </Button>
</Dialog.Footer> </Dialog.Footer>
</Dialog> </Dialog>

View file

@ -161,13 +161,38 @@ export const scriptRuleConditions = (rule: ScriptRule): ScriptRuleCondition[] =>
export const DEFAULT_SCRIPT_OPERATORS: ScriptOperator[] = [ export const DEFAULT_SCRIPT_OPERATORS: ScriptOperator[] = [
{ value: 'always', label: 'Her zaman' }, { value: 'always', label: 'Her zaman' },
{ value: 'equals', label: 'Eşitse', needsValue: true, needsSource: true }, {
{ value: 'notEquals', label: 'Eşit değilse', needsValue: true, needsSource: true }, value: 'equals',
{ value: 'contains', label: 'İçeriyorsa', needsValue: true, needsSource: true }, label: 'App.ScriptBuilder.Operator.Equals',
{ value: 'empty', label: 'Boşsa', needsSource: true }, needsValue: true,
needsSource: true,
},
{
value: 'notEquals',
label: 'App.ScriptBuilder.Operator.NotEquals',
needsValue: true,
needsSource: true,
},
{
value: 'contains',
label: 'App.ScriptBuilder.Operator.Contains',
needsValue: true,
needsSource: true,
},
{ value: 'empty', label: 'App.ScriptBuilder.Operator.Empty', needsSource: true },
{ value: 'notEmpty', label: 'Doluysa', needsSource: true }, { value: 'notEmpty', label: 'Doluysa', needsSource: true },
{ value: 'greaterThan', label: 'Büyükse', needsValue: true, needsSource: true }, {
{ value: 'lessThan', label: 'Küçükse', needsValue: true, needsSource: true }, value: 'greaterThan',
{ value: 'isTrue', label: 'İşaretliyse', needsSource: true }, label: 'App.ScriptBuilder.Operator.GreaterThan',
{ value: 'isFalse', label: 'İşaretli değilse', needsSource: true }, needsValue: true,
needsSource: true,
},
{
value: 'lessThan',
label: 'App.ScriptBuilder.Operator.LessThan',
needsValue: true,
needsSource: true,
},
{ value: 'isTrue', label: 'App.ScriptBuilder.Operator.IsTrue', needsSource: true },
{ value: 'isFalse', label: 'App.ScriptBuilder.Operator.IsFalse', needsSource: true },
] ]

View file

@ -29,7 +29,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
return null return null
} }
const label = translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate' const label = translate('::ListForms.ListForm.DbMigrate')
return ( return (
<> <>
@ -39,7 +39,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
icon={<FcAcceptDatabase />} icon={<FcAcceptDatabase />}
className={className} className={className}
onClick={() => setIsConfirmOpen(true)} onClick={() => setIsConfirmOpen(true)}
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'} title={translate('::App.DbMigrate.StartMessage')}
> >
{hideLabelOnMobile ? <span className="hidden sm:inline">{label}</span> : label} {hideLabelOnMobile ? <span className="hidden sm:inline">{label}</span> : label}
</Button> </Button>
@ -49,7 +49,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
type="info" type="info"
title={label} title={label}
cancelText={translate('::Cancel')} cancelText={translate('::Cancel')}
confirmText={translate('::App.Platform.Execute') || 'Çalıştır'} confirmText={translate('::App.Platform.Execute')}
onCancel={() => setIsConfirmOpen(false)} onCancel={() => setIsConfirmOpen(false)}
onClose={() => setIsConfirmOpen(false)} onClose={() => setIsConfirmOpen(false)}
onConfirm={() => { onConfirm={() => {
@ -58,8 +58,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
}} }}
> >
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
{translate('::App.DbMigrate.ConfirmMessage') || {translate('::App.DbMigrate.ConfirmMessage')}
'Are you sure you want to start the database migration process?'}
</p> </p>
</ConfirmDialog> </ConfirmDialog>
</> </>

View file

@ -44,9 +44,7 @@ const TenantSelector = () => {
setTenantName(undefined) setTenantName(undefined)
const parts = window.location.hostname.split('.') const parts = window.location.hostname.split('.')
const mainDomain = parts.length >= 3 ? parts.slice(1).join('.') : window.location.hostname const mainDomain = parts.length >= 3 ? parts.slice(1).join('.') : window.location.hostname
setWarningTimeout( setWarningTimeout(translate('::App.Tenant.NotFoundRedirect', { name }))
`"${name}" kurumuna ait kayıt bulunamadı.\nAna sayfaya yönlendiriliyorsunuz...`,
)
setTimeout(() => { setTimeout(() => {
window.location.href = `${window.location.protocol}//${mainDomain}` window.location.href = `${window.location.protocol}//${mainDomain}`
}, 3000) }, 3000)

View file

@ -16,6 +16,7 @@ import { useInputGroup } from '../InputGroup/context'
import { CONTROL_SIZES } from '../utils/constants' import { CONTROL_SIZES } from '../utils/constants'
import { Spinner } from '../Spinner' import { Spinner } from '../Spinner'
import type { CommonProps, TypeAttributes } from '../@types/common' import type { CommonProps, TypeAttributes } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
// ── Tipler ────────────────────────────────────────────────────────────────── // ── Tipler ──────────────────────────────────────────────────────────────────
@ -73,15 +74,10 @@ export interface AutoCompleteProps extends CommonProps {
function defaultFilter(options: AutoCompleteOption[], query: string) { function defaultFilter(options: AutoCompleteOption[], query: string) {
const lower = query.toLowerCase() const lower = query.toLowerCase()
return options.filter( return options.filter((o) => !o.disabled && o.label.toLowerCase().includes(lower))
(o) => !o.disabled && o.label.toLowerCase().includes(lower),
)
} }
function useDebounce<T extends (...args: Parameters<T>) => void>( function useDebounce<T extends (...args: Parameters<T>) => void>(fn: T, delay: number) {
fn: T,
delay: number,
) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null) const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
return useCallback( return useCallback(
(...args: Parameters<T>) => { (...args: Parameters<T>) => {
@ -94,8 +90,8 @@ function useDebounce<T extends (...args: Parameters<T>) => void>(
// ── Komponent ──────────────────────────────────────────────────────────────── // ── Komponent ────────────────────────────────────────────────────────────────
const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>( const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>((props, ref) => {
(props, ref) => { const { translate } = useLocalization()
const { const {
className, className,
style, style,
@ -109,8 +105,8 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
fetchOptions, fetchOptions,
debounce: debounceMs = 300, debounce: debounceMs = 300,
renderOption, renderOption,
noOptionsText = 'Seçenek bulunamadı', noOptionsText,
loadingText = 'Yükleniyor...', loadingText,
onInputChange, onInputChange,
onSelect, onSelect,
clearable = true, clearable = true,
@ -127,9 +123,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
) )
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(-1) const [activeIndex, setActiveIndex] = useState(-1)
const [filteredOptions, setFilteredOptions] = useState< const [filteredOptions, setFilteredOptions] = useState<AutoCompleteOption[]>([])
AutoCompleteOption[]
>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@ -139,8 +133,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
const { themeColor, primaryColorLevel, controlSize } = useConfig() const { themeColor, primaryColorLevel, controlSize } = useConfig()
const formControlSize = useForm()?.size const formControlSize = useForm()?.size
const inputGroupSize = useInputGroup()?.size const inputGroupSize = useInputGroup()?.size
const resolvedSize = const resolvedSize = size || inputGroupSize || formControlSize || controlSize
size || inputGroupSize || formControlSize || controlSize
const sizeClass = `h-${CONTROL_SIZES[resolvedSize]}` const sizeClass = `h-${CONTROL_SIZES[resolvedSize]}`
// Sync controlled value // Sync controlled value
@ -156,10 +149,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
// Dışarı tıkla → kapat // Dışarı tıkla → kapat
useEffect(() => { useEffect(() => {
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
if ( if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false) setOpen(false)
setActiveIndex(-1) setActiveIndex(-1)
} }
@ -237,9 +227,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
switch (e.key) { switch (e.key) {
case 'ArrowDown': case 'ArrowDown':
e.preventDefault() e.preventDefault()
setActiveIndex((i) => setActiveIndex((i) => Math.min(i + 1, filteredOptions.length - 1))
Math.min(i + 1, filteredOptions.length - 1),
)
break break
case 'ArrowUp': case 'ArrowUp':
e.preventDefault() e.preventDefault()
@ -280,11 +268,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
const invalidClass = 'border-red-500 focus:ring-red-500' const invalidClass = 'border-red-500 focus:ring-red-500'
return ( return (
<div <div ref={containerRef} className={classNames('autocomplete', className)} style={style}>
ref={containerRef}
className={classNames('autocomplete', className)}
style={style}
>
<div className="autocomplete-input-wrapper"> <div className="autocomplete-input-wrapper">
<input <input
ref={ref} ref={ref}
@ -298,38 +282,27 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
sizeClass, sizeClass,
invalid ? invalidClass : ringClass, invalid ? invalidClass : ringClass,
disabled && 'autocomplete-input-disabled', disabled && 'autocomplete-input-disabled',
clearable && clearable && inputValue && 'autocomplete-input-clearable',
inputValue &&
'autocomplete-input-clearable',
)} )}
role="combobox" role="combobox"
aria-autocomplete="list" aria-autocomplete="list"
aria-expanded={open} aria-expanded={open}
aria-controls={listboxId} aria-controls={listboxId}
aria-activedescendant={ aria-activedescendant={activeIndex >= 0 ? `autocomplete-opt-${activeIndex}` : undefined}
activeIndex >= 0
? `autocomplete-opt-${activeIndex}`
: undefined
}
onChange={handleChange} onChange={handleChange}
onFocus={handleFocus} onFocus={handleFocus}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
{...rest} {...rest}
/> />
<div className="autocomplete-suffix"> <div className="autocomplete-suffix">
{loading && ( {loading && <Spinner size={14} className="autocomplete-spinner" />}
<Spinner
size={14}
className="autocomplete-spinner"
/>
)}
{clearable && inputValue && !loading && ( {clearable && inputValue && !loading && (
<button <button
type="button" type="button"
className="autocomplete-clear" className="autocomplete-clear"
onClick={handleClear} onClick={handleClear}
tabIndex={-1} tabIndex={-1}
aria-label="Temizle" aria-label={translate('::App.DeveloperKit.CrudEndpoints.Clear')}
> >
<svg <svg
width={12} width={12}
@ -348,19 +321,14 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
</div> </div>
{open && ( {open && (
<ul <ul ref={listRef} id={listboxId} role="listbox" className="autocomplete-dropdown">
ref={listRef}
id={listboxId}
role="listbox"
className="autocomplete-dropdown"
>
{loading ? ( {loading ? (
<li className="autocomplete-option autocomplete-option-info"> <li className="autocomplete-option autocomplete-option-info">
{loadingText} {loadingText ?? translate('::App.Loading')}
</li> </li>
) : filteredOptions.length === 0 ? ( ) : filteredOptions.length === 0 ? (
<li className="autocomplete-option autocomplete-option-info"> <li className="autocomplete-option autocomplete-option-info">
{noOptionsText} {noOptionsText ?? translate('::App.AutoComplete.NoOptions')}
</li> </li>
) : ( ) : (
filteredOptions.map((option, index) => ( filteredOptions.map((option, index) => (
@ -372,21 +340,16 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
aria-disabled={option.disabled} aria-disabled={option.disabled}
className={classNames( className={classNames(
'autocomplete-option', 'autocomplete-option',
activeIndex === index && activeIndex === index && 'autocomplete-option-active',
'autocomplete-option-active', option.disabled && 'autocomplete-option-disabled',
option.disabled &&
'autocomplete-option-disabled',
)} )}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => { onClick={() => {
if (!option.disabled) if (!option.disabled) commitSelection(option)
commitSelection(option)
}} }}
onMouseEnter={() => setActiveIndex(index)} onMouseEnter={() => setActiveIndex(index)}
> >
{renderOption {renderOption ? renderOption(option, activeIndex === index) : option.label}
? renderOption(option, activeIndex === index)
: option.label}
</li> </li>
)) ))
)} )}
@ -394,8 +357,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
)} )}
</div> </div>
) )
}, })
)
AutoComplete.displayName = 'AutoComplete' AutoComplete.displayName = 'AutoComplete'

View file

@ -1,12 +1,7 @@
import React, { import React, { Children, cloneElement, forwardRef, isValidElement, type ReactNode } from 'react'
Children,
cloneElement,
forwardRef,
isValidElement,
type ReactNode,
} from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import type { CommonProps } from '../@types/common' import type { CommonProps } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
export interface BreadcrumbItemProps extends CommonProps { export interface BreadcrumbItemProps extends CommonProps {
/** Tıklanabilir link href */ /** Tıklanabilir link href */
@ -32,8 +27,7 @@ export interface BreadcrumbProps extends CommonProps {
size?: 'sm' | 'md' | 'lg' size?: 'sm' | 'md' | 'lg'
} }
export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>( export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>((props, ref) => {
(props, ref) => {
const { const {
className, className,
children, children,
@ -51,11 +45,7 @@ export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>(
return ( return (
<li <li
ref={ref} ref={ref}
className={classNames( className={classNames('breadcrumb-item', active && 'breadcrumb-item-active', className)}
'breadcrumb-item',
active && 'breadcrumb-item-active',
className,
)}
style={style} style={style}
aria-current={active ? 'page' : undefined} aria-current={active ? 'page' : undefined}
{...rest} {...rest}
@ -64,26 +54,22 @@ export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>(
href={href} href={href}
className={classNames( className={classNames(
'breadcrumb-link', 'breadcrumb-link',
active active ? 'breadcrumb-link-active' : 'breadcrumb-link-default',
? 'breadcrumb-link-active'
: 'breadcrumb-link-default',
onClick && !active && 'cursor-pointer', onClick && !active && 'cursor-pointer',
)} )}
onClick={onClick} onClick={onClick}
> >
{icon && ( {icon && <span className="breadcrumb-icon">{icon}</span>}
<span className="breadcrumb-icon">{icon}</span>
)}
{children} {children}
</Tag> </Tag>
</li> </li>
) )
}, })
)
BreadcrumbItem.displayName = 'BreadcrumbItem' BreadcrumbItem.displayName = 'BreadcrumbItem'
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => { const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
const { translate } = useLocalization()
const { const {
className, className,
children, children,
@ -103,17 +89,12 @@ const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
if (maxItems && total > maxItems) { if (maxItems && total > maxItems) {
collapsedCount = total - maxItems collapsedCount = total - maxItems
visibleItems = [ visibleItems = [items[0], ...items.slice(total - (maxItems - 1))]
items[0],
...items.slice(total - (maxItems - 1)),
]
} }
const renderedItems = visibleItems.map((item, index) => { const renderedItems = visibleItems.map((item, index) => {
const isLast = const isLast =
maxItems && collapsedCount > 0 maxItems && collapsedCount > 0 ? index === visibleItems.length - 1 : index === total - 1
? index === visibleItems.length - 1
: index === total - 1
const cloned = cloneElement(item as React.ReactElement<BreadcrumbItemProps>, { const cloned = cloneElement(item as React.ReactElement<BreadcrumbItemProps>, {
active: isLast, active: isLast,
@ -123,27 +104,19 @@ const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
<span key={index} className="breadcrumb-entry" style={{ gap }}> <span key={index} className="breadcrumb-entry" style={{ gap }}>
{index === 1 && collapsedCount > 0 && ( {index === 1 && collapsedCount > 0 && (
<> <>
<span <span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
className="breadcrumb-separator"
aria-hidden
style={{ marginInline: gap }}
>
{separator} {separator}
</span> </span>
<span <span
className="breadcrumb-collapsed" className="breadcrumb-collapsed"
title={`${collapsedCount} öğe gizlendi`} title={translate('::App.Breadcrumb.CollapsedCount', { count: collapsedCount })}
> >
... ...
</span> </span>
</> </>
)} )}
{index > 0 && ( {index > 0 && (
<span <span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
className="breadcrumb-separator"
aria-hidden
style={{ marginInline: gap }}
>
{separator} {separator}
</span> </span>
)} )}
@ -156,11 +129,7 @@ const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
<nav <nav
ref={ref} ref={ref}
aria-label="breadcrumb" aria-label="breadcrumb"
className={classNames( className={classNames('breadcrumb-nav', `breadcrumb-${size}`, className)}
'breadcrumb-nav',
`breadcrumb-${size}`,
className,
)}
style={style} style={style}
{...rest} {...rest}
> >

View file

@ -13,6 +13,7 @@ import { useForm } from '../Form/context'
import { useInputGroup } from '../InputGroup/context' import { useInputGroup } from '../InputGroup/context'
import { CONTROL_SIZES } from '../utils/constants' import { CONTROL_SIZES } from '../utils/constants'
import type { CommonProps, TypeAttributes } from '../@types/common' import type { CommonProps, TypeAttributes } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
export interface ChipsProps extends CommonProps { export interface ChipsProps extends CommonProps {
/** Kontrollü değer */ /** Kontrollü değer */
@ -50,6 +51,7 @@ export interface ChipsProps extends CommonProps {
} }
const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => { const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
const { translate } = useLocalization()
const { const {
className, className,
style, style,
@ -208,18 +210,26 @@ const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
onKeyDown={(e) => handleChipKeyDown(e, i)} onKeyDown={(e) => handleChipKeyDown(e, i)}
data-focused={focusedChip === i} data-focused={focusedChip === i}
> >
<span className="chips-item-label"> <span className="chips-item-label">{itemTemplate ? itemTemplate(chip) : chip}</span>
{itemTemplate ? itemTemplate(chip) : chip}
</span>
{!disabled && ( {!disabled && (
<button <button
type="button" type="button"
className="chips-item-remove" className="chips-item-remove"
onClick={(e) => { e.stopPropagation(); removeChip(i) }} onClick={(e) => {
e.stopPropagation()
removeChip(i)
}}
tabIndex={-1} tabIndex={-1}
aria-label={`${chip} kaldır`} aria-label={translate('::App.Chips.RemoveChip', { chip })}
>
<svg
width={10}
height={10}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
> >
<svg width={10} height={10} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5}>
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@ -237,7 +247,7 @@ const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
placeholder={chips.length === 0 ? placeholder : undefined} placeholder={chips.length === 0 ? placeholder : undefined}
disabled={disabled} disabled={disabled}
autoComplete="off" autoComplete="off"
aria-label="Chip ekle" aria-label={translate('::App.Chips.AddChip')}
onChange={(e) => setInputVal(e.target.value)} onChange={(e) => setInputVal(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onBlur={() => { onBlur={() => {

View file

@ -8,6 +8,7 @@ import React, {
} from 'react' } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import type { CommonProps, TypeAttributes } from '../@types/common' import type { CommonProps, TypeAttributes } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
// ── Hex/RGB yardımcı fonksiyonlar ─────────────────────────────────────────── // ── Hex/RGB yardımcı fonksiyonlar ───────────────────────────────────────────
@ -31,10 +32,7 @@ const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
const rgbToHex = (r: number, g: number, b: number): string => { const rgbToHex = (r: number, g: number, b: number): string => {
return ( return (
'#' + '#' + [r, g, b].map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, '0')).join('')
[r, g, b]
.map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, '0'))
.join('')
) )
} }
@ -72,6 +70,7 @@ export interface ColorPickerProps extends CommonProps {
// ── Bileşen ───────────────────────────────────────────────────────────────── // ── Bileşen ─────────────────────────────────────────────────────────────────
const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => { const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
const { translate } = useLocalization()
const { const {
className, className,
style, style,
@ -106,7 +105,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
const nativeRef = useRef<HTMLInputElement>(null) const nativeRef = useRef<HTMLInputElement>(null)
const currentHex = (() => { const currentHex = (() => {
const src = isControlled ? valueProp! : fieldValue ?? internalHex const src = isControlled ? valueProp! : (fieldValue ?? internalHex)
return isValidHex(src) ? src : internalHex return isValidHex(src) ? src : internalHex
})() })()
@ -119,10 +118,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
if ( if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false) setOpen(false)
} }
} }
@ -187,9 +183,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
return ( return (
<div <div
ref={(node) => { ref={(node) => {
;( ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
containerRef as React.MutableRefObject<HTMLDivElement | null>
).current = node
if (typeof ref === 'function') ref(node) if (typeof ref === 'function') ref(node)
else if (ref) ref.current = node else if (ref) ref.current = node
}} }}
@ -213,13 +207,10 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
{/* Tetikleyici swatch */} {/* Tetikleyici swatch */}
<button <button
type="button" type="button"
className={classNames( className={classNames('color-picker-swatch', disabled && 'color-picker-disabled')}
'color-picker-swatch',
disabled && 'color-picker-disabled',
)}
style={{ backgroundColor: currentHex }} style={{ backgroundColor: currentHex }}
disabled={disabled} disabled={disabled}
aria-label="Renk seç" aria-label={translate('::App.ColorPicker.PickColor')}
onClick={() => { onClick={() => {
if (disabled) return if (disabled) return
setOpen((o) => !o) setOpen((o) => !o)
@ -248,8 +239,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
type="button" type="button"
className={classNames( className={classNames(
'color-picker-preset-dot', 'color-picker-preset-dot',
currentHex.toLowerCase() === currentHex.toLowerCase() === color.toLowerCase() &&
color.toLowerCase() &&
'color-picker-preset-dot-active', 'color-picker-preset-dot-active',
)} )}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
@ -287,13 +277,9 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) =>
min={0} min={0}
max={255} max={255}
value={rgb[ch]} value={rgb[ch]}
onChange={(e) => onChange={(e) => handleRgbChange(ch, e.target.value)}
handleRgbChange(ch, e.target.value)
}
/> />
<span className="color-picker-input-label"> <span className="color-picker-input-label">{ch.toUpperCase()}</span>
{ch.toUpperCase()}
</span>
</div> </div>
))} ))}
</div> </div>

View file

@ -11,6 +11,7 @@ import {
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import classNames from 'classnames' import classNames from 'classnames'
import type { CommonProps } from '../@types/common' import type { CommonProps } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
// ── Tipler ────────────────────────────────────────────────────────────────── // ── Tipler ──────────────────────────────────────────────────────────────────
@ -138,6 +139,7 @@ const ImageViewerOverlay = ({
onClose: () => void onClose: () => void
onIndexChange: (i: number) => void onIndexChange: (i: number) => void
}) => { }) => {
const { translate } = useLocalization()
const [zoom, setZoom] = useState(1) const [zoom, setZoom] = useState(1)
const [rotation, setRotation] = useState(0) const [rotation, setRotation] = useState(0)
const [dragging, setDragging] = useState(false) const [dragging, setDragging] = useState(false)
@ -156,7 +158,9 @@ const ImageViewerOverlay = ({
}, []) }, [])
// Reset on image change // Reset on image change
useEffect(() => { resetTransform() }, [index, resetTransform]) useEffect(() => {
resetTransform()
}, [index, resetTransform])
const goNext = useCallback(() => { const goNext = useCallback(() => {
if (!hasNext) return if (!hasNext) return
@ -168,10 +172,14 @@ const ImageViewerOverlay = ({
onIndexChange(loop ? (index - 1 + images.length) % images.length : index - 1) onIndexChange(loop ? (index - 1 + images.length) % images.length : index - 1)
}, [hasPrev, loop, index, images.length, onIndexChange]) }, [hasPrev, loop, index, images.length, onIndexChange])
const zoomIn = useCallback(() => const zoomIn = useCallback(
setZoom((z) => Math.min(z + zoomStep, maxZoom)), [zoomStep, maxZoom]) () => setZoom((z) => Math.min(z + zoomStep, maxZoom)),
const zoomOut = useCallback(() => [zoomStep, maxZoom],
setZoom((z) => Math.max(z - zoomStep, minZoom)), [zoomStep, minZoom]) )
const zoomOut = useCallback(
() => setZoom((z) => Math.max(z - zoomStep, minZoom)),
[zoomStep, minZoom],
)
const rotateCW = useCallback(() => setRotation((r) => r + 90), []) const rotateCW = useCallback(() => setRotation((r) => r + 90), [])
const rotateCCW = useCallback(() => setRotation((r) => r - 90), []) const rotateCCW = useCallback(() => setRotation((r) => r - 90), [])
@ -184,9 +192,7 @@ const ImageViewerOverlay = ({
const mimeMatch = src.match(/^data:([^;,]+)/) const mimeMatch = src.match(/^data:([^;,]+)/)
const mime = mimeMatch?.[1] ?? 'image/png' const mime = mimeMatch?.[1] ?? 'image/png'
const ext = mime.split('/')[1]?.replace('jpeg', 'jpg') ?? 'png' const ext = mime.split('/')[1]?.replace('jpeg', 'jpg') ?? 'png'
const baseName = current.alt const baseName = current.alt ? current.alt.replace(/\.[^.]+$/, '') : 'image'
? current.alt.replace(/\.[^.]+$/, '')
: 'image'
const a = document.createElement('a') const a = document.createElement('a')
a.href = src a.href = src
a.download = `${baseName}.${ext}` a.download = `${baseName}.${ext}`
@ -208,8 +214,7 @@ const ImageViewerOverlay = ({
a.download = fileName a.download = fileName
// Aynı origin değilse fetch + blob ile zorla indir // Aynı origin değilse fetch + blob ile zorla indir
try { try {
const isSameOrigin = const isSameOrigin = new URL(src, window.location.href).origin === window.location.origin
new URL(src, window.location.href).origin === window.location.origin
if (!isSameOrigin) { if (!isSameOrigin) {
fetch(src) fetch(src)
.then((r) => r.blob()) .then((r) => r.blob())
@ -221,7 +226,9 @@ const ImageViewerOverlay = ({
}) })
return return
} }
} catch { /* origin parse hatası — direkt dene */ } } catch {
/* origin parse hatası — direkt dene */
}
a.click() a.click()
} }
}, [current]) }, [current])
@ -230,13 +237,28 @@ const ImageViewerOverlay = ({
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case 'ArrowLeft': goPrev(); break case 'ArrowLeft':
case 'ArrowRight': goNext(); break goPrev()
case 'Escape': onClose(); break break
case '+': case '=': zoomIn(); break case 'ArrowRight':
case '-': zoomOut(); break goNext()
case 'r': rotateCW(); break break
case 'R': rotateCCW(); break case 'Escape':
onClose()
break
case '+':
case '=':
zoomIn()
break
case '-':
zoomOut()
break
case 'r':
rotateCW()
break
case 'R':
rotateCCW()
break
} }
} }
window.addEventListener('keydown', handler) window.addEventListener('keydown', handler)
@ -264,7 +286,10 @@ const ImageViewerOverlay = ({
y: dragStart.current.oy + (e.clientY - dragStart.current.y), y: dragStart.current.oy + (e.clientY - dragStart.current.y),
}) })
} }
const handleMouseUp = () => { setDragging(false); dragStart.current = null } const handleMouseUp = () => {
setDragging(false)
dragStart.current = null
}
const overlayClick = (e: MouseEvent<HTMLDivElement>) => { const overlayClick = (e: MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) onClose() if (e.target === e.currentTarget) onClose()
@ -275,7 +300,7 @@ const ImageViewerOverlay = ({
className="image-viewer-overlay" className="image-viewer-overlay"
role="dialog" role="dialog"
aria-modal aria-modal
aria-label={current.alt ?? 'Resim görüntüleyici'} aria-label={current.alt ?? translate('::App.ImageViewer.Title')}
> >
{/* Toolbar */} {/* Toolbar */}
{showToolbar && ( {showToolbar && (
@ -283,33 +308,42 @@ const ImageViewerOverlay = ({
<span className="image-viewer-counter"> <span className="image-viewer-counter">
{index + 1} / {images.length} {index + 1} / {images.length}
</span> </span>
{current.caption && ( {current.caption && <span className="image-viewer-caption">{current.caption}</span>}
<span className="image-viewer-caption">{current.caption}</span>
)}
<div className="image-viewer-toolbar-actions"> <div className="image-viewer-toolbar-actions">
{toolbarExtra} {toolbarExtra}
<button type="button" onClick={zoomOut} title="Uzaklaştır (-)"> <button type="button" onClick={zoomOut} title={translate('::App.Platform.ZoomOut')}>
<IconZoomOut /> <IconZoomOut />
</button> </button>
<span className="image-viewer-zoom-label"> <span className="image-viewer-zoom-label">{Math.round(zoom * 100)}%</span>
{Math.round(zoom * 100)}% <button type="button" onClick={zoomIn} title={translate('::App.Platform.ZoomIn')}>
</span>
<button type="button" onClick={zoomIn} title="Yaklaştır (+)">
<IconZoomIn /> <IconZoomIn />
</button> </button>
<button type="button" onClick={resetTransform} title="Sıfırla"> <button type="button" onClick={resetTransform} title={translate('::Reset')}>
<IconReset /> <IconReset />
</button> </button>
<button type="button" onClick={rotateCCW} title="Sola döndür (R)"> <button
type="button"
onClick={rotateCCW}
title={translate('::App.ImageViewer.RotateLeft')}
>
<IconRotateCCW /> <IconRotateCCW />
</button> </button>
<button type="button" onClick={rotateCW} title="Sağa döndür (r)"> <button
type="button"
onClick={rotateCW}
title={translate('::App.ImageViewer.RotateRight')}
>
<IconRotateCW /> <IconRotateCW />
</button> </button>
<button type="button" onClick={handleDownload} title="İndir"> <button type="button" onClick={handleDownload} title={translate('::Download')}>
<IconDownload /> <IconDownload />
</button> </button>
<button type="button" onClick={onClose} title="Kapat (Esc)" className="image-viewer-close"> <button
type="button"
onClick={onClose}
title={translate('::App.Platform.CloseWithEsc')}
className="image-viewer-close"
>
<IconClose /> <IconClose />
</button> </button>
</div> </div>
@ -318,10 +352,7 @@ const ImageViewerOverlay = ({
{/* Stage */} {/* Stage */}
<div <div
className={classNames( className={classNames('image-viewer-stage', dragging && 'image-viewer-dragging')}
'image-viewer-stage',
dragging && 'image-viewer-dragging',
)}
onClick={overlayClick} onClick={overlayClick}
onWheel={handleWheel} onWheel={handleWheel}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
@ -348,7 +379,7 @@ const ImageViewerOverlay = ({
type="button" type="button"
className="image-viewer-nav image-viewer-nav-prev" className="image-viewer-nav image-viewer-nav-prev"
onClick={goPrev} onClick={goPrev}
aria-label="Önceki" aria-label={translate('::App.VisualDesigner.Canvas.Previous')}
> >
<IconPrev /> <IconPrev />
</button> </button>
@ -358,7 +389,7 @@ const ImageViewerOverlay = ({
type="button" type="button"
className="image-viewer-nav image-viewer-nav-next" className="image-viewer-nav image-viewer-nav-next"
onClick={goNext} onClick={goNext}
aria-label="Sonraki" aria-label={translate('::App.VisualDesigner.Canvas.Next')}
> >
<IconNext /> <IconNext />
</button> </button>
@ -376,14 +407,10 @@ const ImageViewerOverlay = ({
'image-viewer-thumb', 'image-viewer-thumb',
i === index && 'image-viewer-thumb-active', i === index && 'image-viewer-thumb-active',
)} )}
aria-label={img.alt ?? `Resim ${i + 1}`} aria-label={img.alt ?? translate('::App.ImageViewer.ImageN', { index: i + 1 })}
aria-pressed={i === index} aria-pressed={i === index}
> >
<img <img src={img.thumbnail ?? img.src} alt={img.alt ?? ''} draggable={false} />
src={img.thumbnail ?? img.src}
alt={img.alt ?? ''}
draggable={false}
/>
</button> </button>
))} ))}
</div> </div>
@ -395,6 +422,7 @@ const ImageViewerOverlay = ({
// ── Ana Komponent ──────────────────────────────────────────────────────────── // ── Ana Komponent ────────────────────────────────────────────────────────────
const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => { const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => {
const { translate } = useLocalization()
const { const {
className, className,
style, style,
@ -421,8 +449,7 @@ const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) =>
const [internalIndex, setInternalIndex] = useState(defaultIndex) const [internalIndex, setInternalIndex] = useState(defaultIndex)
const isOpen = isControlled ? openProp! : internalOpen const isOpen = isControlled ? openProp! : internalOpen
const currentIndex = const currentIndex = activeIndexProp !== undefined ? activeIndexProp : internalIndex
activeIndexProp !== undefined ? activeIndexProp : internalIndex
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (!isControlled) setInternalOpen(false) if (!isControlled) setInternalOpen(false)
@ -463,13 +490,9 @@ const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) =>
type="button" type="button"
className="image-viewer-trigger-item" className="image-viewer-trigger-item"
onClick={() => openAt(i)} onClick={() => openAt(i)}
aria-label={img.alt ?? `Resim ${i + 1}`} aria-label={img.alt ?? translate('::App.ImageViewer.ImageN', { index: i + 1 })}
> >
<img <img src={img.thumbnail ?? img.src} alt={img.alt ?? ''} draggable={false} />
src={img.thumbnail ?? img.src}
alt={img.alt ?? ''}
draggable={false}
/>
</button> </button>
))} ))}
</div> </div>

View file

@ -52,11 +52,9 @@ const KNOB_START_ANGLE = -220 // derece (saat 7 hizası)
const KNOB_END_ANGLE = 40 // derece (saat 5 hizası) const KNOB_END_ANGLE = 40 // derece (saat 5 hizası)
const KNOB_RANGE = KNOB_END_ANGLE - KNOB_START_ANGLE // 260 derece toplam yay const KNOB_RANGE = KNOB_END_ANGLE - KNOB_START_ANGLE // 260 derece toplam yay
const clamp = (v: number, min: number, max: number) => const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
Math.min(max, Math.max(min, v))
const snap = (v: number, step: number, min: number) => const snap = (v: number, step: number, min: number) => Math.round((v - min) / step) * step + min
Math.round((v - min) / step) * step + min
const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => { const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
const { const {
@ -93,13 +91,10 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
}, [isControlled, valueProp, min, max]) }, [isControlled, valueProp, min, max])
useEffect(() => { useEffect(() => {
if (field?.value !== undefined) if (field?.value !== undefined) setInternalValue(clamp(field.value, min, max))
setInternalValue(clamp(field.value, min, max))
}, [field?.value, min, max]) }, [field?.value, min, max])
const currentValue = isControlled const currentValue = isControlled ? clamp(valueProp!, min, max) : internalValue
? clamp(valueProp!, min, max)
: internalValue
const commit = useCallback( const commit = useCallback(
(raw: number) => { (raw: number) => {
@ -119,7 +114,7 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
const valueRatio = (currentValue - min) / (max - min) const valueRatio = (currentValue - min) / (max - min)
const valueAngleDeg = KNOB_START_ANGLE + valueRatio * KNOB_RANGE const valueAngleDeg = KNOB_START_ANGLE + valueRatio * KNOB_RANGE
const valueArcLength = (valueRatio * KNOB_RANGE / 360) * circumference const valueArcLength = ((valueRatio * KNOB_RANGE) / 360) * circumference
const fullArcLength = (KNOB_RANGE / 360) * circumference const fullArcLength = (KNOB_RANGE / 360) * circumference
// Çember yayını SVG stroke-dashoffset ile çiziyoruz // Çember yayını SVG stroke-dashoffset ile çiziyoruz
@ -129,8 +124,7 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
const resolveValueColor = () => const resolveValueColor = () =>
valueColor ?? `var(--color-${themeColor}-${primaryColorLevel}, #6366f1)` valueColor ?? `var(--color-${themeColor}-${primaryColorLevel}, #6366f1)`
const resolveRangeColor = () => const resolveRangeColor = () => rangeColor ?? `currentColor`
rangeColor ?? `currentColor`
const resolveTextColor = () => textColor ?? undefined const resolveTextColor = () => textColor ?? undefined
const label = valueTemplate.replace('{value}', String(currentValue)) const label = valueTemplate.replace('{value}', String(currentValue))
@ -150,8 +144,7 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
[min, max], [min, max],
) )
const getAngleFromPointer = useCallback( const getAngleFromPointer = useCallback((clientX: number, clientY: number) => {
(clientX: number, clientY: number) => {
const el = svgRef.current const el = svgRef.current
if (!el) return 0 if (!el) return 0
const rect = el.getBoundingClientRect() const rect = el.getBoundingClientRect()
@ -166,9 +159,7 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
// Döngü düzeltmesi // Döngü düzeltmesi
} }
return angle return angle
}, }, [])
[],
)
const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => { const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => {
if (disabled || readOnly) return if (disabled || readOnly) return
@ -230,9 +221,7 @@ const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
return ( return (
<span className={classNames('knob-wrapper', className)} style={style}> <span className={classNames('knob-wrapper', className)} style={style}>
{name && ( {name && <input type="hidden" name={name ?? field?.name} value={currentValue} />}
<input type="hidden" name={name ?? field?.name} value={currentValue} />
)}
<svg <svg
ref={(node) => { ref={(node) => {
;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node ;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node

View file

@ -1,11 +1,4 @@
import React, { import React, { forwardRef, useRef, useState, useEffect, useCallback, Fragment } from 'react'
forwardRef,
useRef,
useState,
useEffect,
useCallback,
Fragment,
} from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import type { CommonProps } from '../@types/common' import type { CommonProps } from '../@types/common'
import type { CSSProperties } from 'react' import type { CSSProperties } from 'react'
@ -75,12 +68,8 @@ const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
const calculateWidth = useCallback(() => { const calculateWidth = useCallback(() => {
if (trackRef.current && containerRef.current) { if (trackRef.current && containerRef.current) {
const tw = isVertical const tw = isVertical ? trackRef.current.offsetHeight : trackRef.current.offsetWidth
? trackRef.current.offsetHeight const cw = isVertical ? containerRef.current.offsetHeight : containerRef.current.offsetWidth
: trackRef.current.offsetWidth
const cw = isVertical
? containerRef.current.offsetHeight
: containerRef.current.offsetWidth
if (autoFill && tw > 0) { if (autoFill && tw > 0) {
setMultiplier(Math.max(Math.ceil((cw * 2) / tw), 2)) setMultiplier(Math.max(Math.ceil((cw * 2) / tw), 2))
@ -123,9 +112,7 @@ const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
...gradientStyle, ...gradientStyle,
...style, ...style,
['--marquee-gradient-width' as string]: ['--marquee-gradient-width' as string]:
typeof gradientWidth === 'number' typeof gradientWidth === 'number' ? `${gradientWidth}px` : gradientWidth,
? `${gradientWidth}px`
: gradientWidth,
} }
const animationStyle: CSSProperties = { const animationStyle: CSSProperties = {
@ -159,8 +146,7 @@ const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
return ( return (
<div <div
ref={(node) => { ref={(node) => {
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
node
if (typeof ref === 'function') { if (typeof ref === 'function') {
ref(node) ref(node)
} else if (ref) { } else if (ref) {
@ -183,11 +169,8 @@ const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
className={classNames( className={classNames(
'marquee-track', 'marquee-track',
animationClass, animationClass,
(pauseOnHover || isPaused) && (pauseOnHover || isPaused) && 'marquee-pause-on-hover',
'marquee-pause-on-hover', (pauseOnClick || isPaused) && isPaused && 'marquee-paused',
(pauseOnClick || isPaused) &&
isPaused &&
'marquee-paused',
)} )}
style={animationStyle} style={animationStyle}
onAnimationIteration={handleAnimationIteration} onAnimationIteration={handleAnimationIteration}
@ -201,8 +184,7 @@ const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
className={classNames( className={classNames(
'marquee-track', 'marquee-track',
animationClass, animationClass,
(pauseOnHover || isPaused) && (pauseOnHover || isPaused) && 'marquee-pause-on-hover',
'marquee-pause-on-hover',
isPaused && 'marquee-paused', isPaused && 'marquee-paused',
)} )}
style={animationStyle} style={animationStyle}

View file

@ -17,8 +17,7 @@ export interface NotificationProps extends CommonProps {
width?: number | string width?: number | string
} }
const Notification = forwardRef<HTMLDivElement, NotificationProps>( const Notification = forwardRef<HTMLDivElement, NotificationProps>((props, ref) => {
(props, ref) => {
const { const {
className, className,
children, children,
@ -38,11 +37,7 @@ const Notification = forwardRef<HTMLDivElement, NotificationProps>(
// tamamlandıktan sonra kendilerini DOM'dan kaldırır. // tamamlandıktan sonra kendilerini DOM'dan kaldırır.
const [hidden, setHidden] = useState(false) const [hidden, setHidden] = useState(false)
const { clear } = useTimeout( const { clear } = useTimeout(onClose as () => void, duration, duration > 0)
onClose as () => void,
duration,
duration > 0
)
const handleClose = useCallback( const handleClose = useCallback(
(e: MouseEvent<HTMLSpanElement>) => { (e: MouseEvent<HTMLSpanElement>) => {
@ -54,7 +49,7 @@ const Notification = forwardRef<HTMLDivElement, NotificationProps>(
}, 400) }, 400)
} }
}, },
[onClose, clear, triggerByToast] [onClose, clear, triggerByToast],
) )
const notificationClass = classNames('notification', className) const notificationClass = classNames('notification', className)
@ -64,18 +59,8 @@ const Notification = forwardRef<HTMLDivElement, NotificationProps>(
} }
return ( return (
<div <div ref={ref} {...rest} className={notificationClass} style={{ width: width, ...style }}>
ref={ref} <div className={classNames('notification-content', !children && 'no-child')}>
{...rest}
className={notificationClass}
style={{ width: width, ...style }}
>
<div
className={classNames(
'notification-content',
!children && 'no-child'
)}
>
{type && !customIcon ? ( {type && !customIcon ? (
<div className="mr-3"> <div className="mr-3">
<StatusIcon type={type} /> <StatusIcon type={type} />
@ -84,18 +69,9 @@ const Notification = forwardRef<HTMLDivElement, NotificationProps>(
{customIcon && <div className="mr-3">{customIcon}</div>} {customIcon && <div className="mr-3">{customIcon}</div>}
<div className="mr-4"> <div className="mr-4">
{title && ( {title && (
<div <div className={classNames('notification-title', !!children && 'mb-1')}>{title}</div>
className={classNames(
'notification-title',
!!children && 'mb-1'
)} )}
> <div className="notification-description">{children}</div>
{title}
</div>
)}
<div className="notification-description">
{children}
</div>
</div> </div>
</div> </div>
{closable && ( {closable && (
@ -108,8 +84,7 @@ const Notification = forwardRef<HTMLDivElement, NotificationProps>(
)} )}
</div> </div>
) )
} })
)
Notification.displayName = 'Notification' Notification.displayName = 'Notification'

View file

@ -9,6 +9,7 @@ import React, {
import classNames from 'classnames' import classNames from 'classnames'
import { useConfig } from '../ConfigProvider' import { useConfig } from '../ConfigProvider'
import type { CommonProps } from '../@types/common' import type { CommonProps } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
export interface RateProps extends CommonProps { export interface RateProps extends CommonProps {
/** Toplam yıldız sayısı. Varsayılan: 5 */ /** Toplam yıldız sayısı. Varsayılan: 5 */
@ -59,13 +60,7 @@ const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size
)} )}
<polygon <polygon
points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"
fill={ fill={half ? 'url(#rate-half)' : filled ? 'currentColor' : 'none'}
half
? 'url(#rate-half)'
: filled
? 'currentColor'
: 'none'
}
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
@ -75,6 +70,7 @@ const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size
) )
const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => { const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
const { translate } = useLocalization()
const { const {
className, className,
style, style,
@ -183,9 +179,7 @@ const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
const starNode = const starNode =
typeof character === 'function' typeof character === 'function'
? character(index) ? character(index)
: character ?? ( : (character ?? <StarIcon filled={filled} half={half} size={size} />)
<StarIcon filled={filled} half={half} size={size} />
)
return ( return (
<span <span
@ -202,7 +196,7 @@ const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
onClick={(e) => handleClick(index, e)} onClick={(e) => handleClick(index, e)}
role={!disabled && !readOnly ? 'radio' : undefined} role={!disabled && !readOnly ? 'radio' : undefined}
aria-checked={displayValue >= index + 1} aria-checked={displayValue >= index + 1}
aria-label={tooltip ?? `${index + 1} yıldız`} aria-label={tooltip ?? translate('::App.Rate.StarCount', { count: index + 1 })}
> >
{starNode} {starNode}
</span> </span>
@ -218,7 +212,7 @@ const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
tabIndex={disabled || readOnly ? undefined : 0} tabIndex={disabled || readOnly ? undefined : 0}
role="radiogroup" role="radiogroup"
aria-label="Değerlendirme" aria-label={translate('::App.Rate.Label')}
{...rest} {...rest}
> >
{Array.from({ length: count }, (_, i) => renderStar(i))} {Array.from({ length: count }, (_, i) => renderStar(i))}

View file

@ -51,8 +51,7 @@ export interface SliderProps extends CommonProps {
} }
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v)) const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
const snap = (v: number, step: number, min: number) => const snap = (v: number, step: number, min: number) => Math.round((v - min) / step) * step + min
Math.round((v - min) / step) * step + min
const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => { const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
const { const {
@ -106,8 +105,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
const currentValue = isControlled ? valueProp! : internalValue const currentValue = isControlled ? valueProp! : internalValue
const toArray = (v: SliderValue): [number, number] => const toArray = (v: SliderValue): [number, number] => (Array.isArray(v) ? v : [v, v])
Array.isArray(v) ? v : [v, v]
const commit = useCallback( const commit = useCallback(
(next: SliderValue, final = false) => { (next: SliderValue, final = false) => {
@ -138,10 +136,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
[isVertical, min, max, step], [isVertical, min, max, step],
) )
const handlePointerDown = ( const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>, handle: 0 | 1) => {
e: ReactPointerEvent<HTMLDivElement>,
handle: 0 | 1,
) => {
if (disabled || readOnly) return if (disabled || readOnly) return
e.currentTarget.setPointerCapture(e.pointerId) e.currentTarget.setPointerCapture(e.pointerId)
activeHandle.current = handle activeHandle.current = handle
@ -160,9 +155,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
if (range) { if (range) {
const [a, b] = toArray(currentValue) const [a, b] = toArray(currentValue)
const next: [number, number] = const next: [number, number] =
activeHandle.current === 0 activeHandle.current === 0 ? [Math.min(newVal, b), b] : [a, Math.max(newVal, a)]
? [Math.min(newVal, b), b]
: [a, Math.max(newVal, a)]
commit(next) commit(next)
} else { } else {
commit(newVal) commit(newVal)
@ -219,19 +212,32 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
switch (e.key) { switch (e.key) {
case 'ArrowRight': case 'ArrowRight':
case 'ArrowUp': case 'ArrowUp':
e.preventDefault(); next = clamp(snap(cur + step, step, min), min, max); break e.preventDefault()
next = clamp(snap(cur + step, step, min), min, max)
break
case 'ArrowLeft': case 'ArrowLeft':
case 'ArrowDown': case 'ArrowDown':
e.preventDefault(); next = clamp(snap(cur - step, step, min), min, max); break e.preventDefault()
next = clamp(snap(cur - step, step, min), min, max)
break
case 'Home': case 'Home':
e.preventDefault(); next = min; break e.preventDefault()
next = min
break
case 'End': case 'End':
e.preventDefault(); next = max; break e.preventDefault()
next = max
break
case 'PageUp': case 'PageUp':
e.preventDefault(); next = clamp(snap(cur + step * 10, step, min), min, max); break e.preventDefault()
next = clamp(snap(cur + step * 10, step, min), min, max)
break
case 'PageDown': case 'PageDown':
e.preventDefault(); next = clamp(snap(cur - step * 10, step, min), min, max); break e.preventDefault()
default: return next = clamp(snap(cur - step * 10, step, min), min, max)
break
default:
return
} }
if (range) { if (range) {
const result: [number, number] = const result: [number, number] =
@ -263,7 +269,8 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
: { left: `calc(${p1}% - ${handleSize / 2}px)` } : { left: `calc(${p1}% - ${handleSize / 2}px)` }
// Marks hesapla // Marks hesapla
const resolvedMarks = marks === true const resolvedMarks =
marks === true
? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({ ? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({
value: min + i * step, value: min + i * step,
label: `${min + i * step}`, label: `${min + i * step}`,
@ -281,11 +288,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
return ( return (
<div <div
key={handle} key={handle}
className={classNames( className={classNames('slider-handle', ringClass, disabled && 'slider-handle-disabled')}
'slider-handle',
ringClass,
disabled && 'slider-handle-disabled',
)}
style={{ ...hStyle, width: handleSize, height: handleSize }} style={{ ...hStyle, width: handleSize, height: handleSize }}
role="slider" role="slider"
tabIndex={disabled || readOnly ? -1 : 0} tabIndex={disabled || readOnly ? -1 : 0}
@ -299,15 +302,22 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
onKeyDown={(e) => handleKeyDown(e, handle)} onKeyDown={(e) => handleKeyDown(e, handle)}
onFocus={() => onFocus={() =>
tooltip && setShowTooltip((t) => { const n = [...t]; n[handle] = true; return n }) tooltip &&
setShowTooltip((t) => {
const n = [...t]
n[handle] = true
return n
})
} }
onBlur={() => onBlur={() =>
setShowTooltip((t) => { const n = [...t]; n[handle] = false; return n }) setShowTooltip((t) => {
const n = [...t]
n[handle] = false
return n
})
} }
> >
{(tooltip || show) && show && ( {(tooltip || show) && show && <div className="slider-tooltip">{val}</div>}
<div className="slider-tooltip">{val}</div>
)}
</div> </div>
) )
} }
@ -334,11 +344,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
<div <div
ref={trackRef} ref={trackRef}
className="slider-track" className="slider-track"
style={ style={isVertical ? { width: trackSize } : { height: trackSize }}
isVertical
? { width: trackSize }
: { height: trackSize }
}
onClick={handleTrackClick} onClick={handleTrackClick}
> >
<div className="slider-fill" style={trackFillStyle} /> <div className="slider-fill" style={trackFillStyle} />
@ -352,21 +358,14 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
<div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}> <div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}>
{resolvedMarks.map((m) => { {resolvedMarks.map((m) => {
const pct = percentOf(m.value) const pct = percentOf(m.value)
const markStyle = isVertical const markStyle = isVertical ? { bottom: `${pct}%` } : { left: `${pct}%` }
? { bottom: `${pct}%` }
: { left: `${pct}%` }
const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0 const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0
return ( return (
<span key={m.value} className="slider-mark-wrapper" style={markStyle}> <span key={m.value} className="slider-mark-wrapper" style={markStyle}>
<span <span
className={classNames( className={classNames('slider-mark-dot', active && 'slider-mark-dot-active')}
'slider-mark-dot',
active && 'slider-mark-dot-active',
)}
/> />
{m.label && ( {m.label && <span className="slider-mark-label">{m.label}</span>}
<span className="slider-mark-label">{m.label}</span>
)}
</span> </span>
) )
})} })}

View file

@ -73,7 +73,7 @@ const fadeMotionProps = {
const scaleTransition = ( const scaleTransition = (
offsetX: number | string, offsetX: number | string,
offsetY: number | string offsetY: number | string,
): Record<NotificationPlacement, MotionTransition> => { ): Record<NotificationPlacement, MotionTransition> => {
return { return {
'top-end': { 'top-end': {
@ -147,7 +147,7 @@ const scaleTransition = (
const fadeTransition = ( const fadeTransition = (
offsetX: number | string, offsetX: number | string,
offsetY: number | string offsetY: number | string,
): Record<NotificationPlacement, MotionTransition> => { ): Record<NotificationPlacement, MotionTransition> => {
return { return {
'top-end': { 'top-end': {

View file

@ -1,5 +1,6 @@
import ScriptBuilderDialog from '@/components/scriptBuilder/ScriptBuilderDialog' import ScriptBuilderDialog from '@/components/scriptBuilder/ScriptBuilderDialog'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { createDesignerScriptDialect } from './designerScriptDialect' import { createDesignerScriptDialect } from './designerScriptDialect'
import type { SqlDataSourceEvent } from './types' import type { SqlDataSourceEvent } from './types'
@ -37,6 +38,8 @@ function DesignerScriptBuilderDialog({
onClose, onClose,
onApply, onApply,
}: DesignerScriptBuilderDialogProps) { }: DesignerScriptBuilderDialogProps) {
const { translate } = useLocalization()
// Çağıran her render'da yeni diziler üretiyor; lehçe içeriklerine göre // Çağıran her render'da yeni diziler üretiyor; lehçe içeriklerine göre
// sabitlenmezse editör yazarken sıfırlanırdı. // sabitlenmezse editör yazarken sıfırlanırdı.
const recordFieldKey = recordFields.join('|') const recordFieldKey = recordFields.join('|')
@ -50,8 +53,9 @@ function DesignerScriptBuilderDialog({
recordFields: recordFieldKey ? recordFieldKey.split('|') : [], recordFields: recordFieldKey ? recordFieldKey.split('|') : [],
refNames: refNameKey ? refNameKey.split('|') : [], refNames: refNameKey ? refNameKey.split('|') : [],
sqlRef, sqlRef,
translate,
}), }),
[componentLabel, eventInfo, eventName, recordFieldKey, refNameKey, sqlRef], [componentLabel, eventInfo, eventName, recordFieldKey, refNameKey, sqlRef, translate],
) )
return ( return (

View file

@ -307,7 +307,8 @@ const buildDesignerRefs = (store: DesignerRefStore | null) => {
return Array.isArray(options) ? options : [] return Array.isArray(options) ? options : []
}, },
getSelectedOption: () => currentOption(), getSelectedOption: () => currentOption(),
getColumn: (path: string) => getDesignerValueByPath(currentOption(), String(path || '')), getColumn: (path: string) =>
getDesignerValueByPath(currentOption(), String(path || '')),
getLabel: () => String(currentOption()?.label ?? ''), getLabel: () => String(currentOption()?.label ?? ''),
getColumnNames: () => getColumnNames: () =>
getDesignerBindingColumns( getDesignerBindingColumns(
@ -362,7 +363,7 @@ const designTimeApi = {
error?.response?.data?.error?.message || error?.response?.data?.error?.message ||
error?.response?.data?.message || error?.response?.data?.message ||
error?.message || error?.message ||
'İşlem tamamlanamadı.', 'App.Platform.OperationCouldNotBeCompleted',
} }
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as FunctionConstructor const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as FunctionConstructor
@ -432,7 +433,7 @@ const toSelectOptions = (
: record[primitiveKeys[1] || primitiveKeys[0]] : record[primitiveKeys[1] || primitiveKeys[0]]
return { return {
...record, ...record,
label: String(labelValue ?? `Seçenek ${index + 1}`), label: String(labelValue ?? `#${index + 1}`),
value: optionValue ?? index, value: optionValue ?? index,
} }
}) })
@ -656,7 +657,10 @@ const getPreviewProps = (
return props return props
} }
const PlatformPlaceholder = ({ node }: { node: DesignerNode }) => ( const PlatformPlaceholder = ({ node }: { node: DesignerNode }) => {
const { translate } = useLocalization()
return (
<div <div
className="flex min-h-40 items-center justify-center rounded-lg border border-dashed border-sky-300 bg-gradient-to-br from-sky-50 to-indigo-50 p-6 text-center dark:border-sky-800 dark:from-slate-900 dark:to-sky-950" className="flex min-h-40 items-center justify-center rounded-lg border border-dashed border-sky-300 bg-gradient-to-br from-sky-50 to-indigo-50 p-6 text-center dark:border-sky-800 dark:from-slate-900 dark:to-sky-950"
style={{ minHeight: String(node.props.height || '320px') }} style={{ minHeight: String(node.props.height || '320px') }}
@ -669,14 +673,17 @@ const PlatformPlaceholder = ({ node }: { node: DesignerNode }) => (
{node.type.replace(/View$/, '')} {node.type.replace(/View$/, '')}
</div> </div>
<div className="mt-1 text-xs text-slate-500"> <div className="mt-1 text-xs text-slate-500">
{String(node.props.listFormCode || 'Property panelinden List Form Code seçin')} {String(
node.props.listFormCode || translate('::App.VisualDesigner.Canvas.SelectListFormCode'),
)}
</div> </div>
<div className="mt-3 text-[11px] uppercase tracking-wider text-sky-700 dark:text-sky-300"> <div className="mt-3 text-[11px] uppercase tracking-wider text-sky-700 dark:text-sky-300">
Platform görünümü {translate('::App.VisualDesigner.Canvas.PlatformView')}
</div> </div>
</div> </div>
</div> </div>
) )
}
const PLATFORM_VIEW_NAMES: Record<string, PlatformViewName> = { const PLATFORM_VIEW_NAMES: Record<string, PlatformViewName> = {
ListView: 'List', ListView: 'List',
@ -690,7 +697,10 @@ const PLATFORM_VIEW_NAMES: Record<string, PlatformViewName> = {
ChartView: 'Chart', ChartView: 'Chart',
} }
const GridColumnHeaders = ({ columns }: { columns: string[] }) => ( const GridColumnHeaders = ({ columns }: { columns: string[] }) => {
const { translate } = useLocalization()
return (
<div className="min-w-0 overflow-auto rounded-md border border-slate-200 bg-slate-100 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-800"> <div className="min-w-0 overflow-auto rounded-md border border-slate-200 bg-slate-100 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-800">
{columns.length ? ( {columns.length ? (
<div className="flex min-w-max divide-x divide-slate-200 dark:divide-slate-700"> <div className="flex min-w-max divide-x divide-slate-200 dark:divide-slate-700">
@ -705,11 +715,12 @@ const GridColumnHeaders = ({ columns }: { columns: string[] }) => (
</div> </div>
) : ( ) : (
<div className="px-3 py-2 text-center text-xs text-slate-400"> <div className="px-3 py-2 text-center text-xs text-slate-400">
Data panelinden gösterilecek sütunları seçin. {translate('::App.VisualDesigner.Canvas.SelectColumnsFromData')}
</div> </div>
)} )}
</div> </div>
) )
}
// Dates and decimals follow the selected language, like the DevExtreme grids do. // Dates and decimals follow the selected language, like the DevExtreme grids do.
const getGridCellText = (value: unknown) => const getGridCellText = (value: unknown) =>
@ -732,6 +743,7 @@ const GridDataTablePreview = ({
overflow?: boolean overflow?: boolean
selectedColumns?: string[] selectedColumns?: string[]
}) => { }) => {
const { translate } = useLocalization()
const firstObject = items.find( const firstObject = items.find(
(item): item is Record<string, unknown> => (item): item is Record<string, unknown> =>
Boolean(item) && typeof item === 'object' && !Array.isArray(item), Boolean(item) && typeof item === 'object' && !Array.isArray(item),
@ -741,7 +753,7 @@ const GridDataTablePreview = ({
if (!columns.length) { if (!columns.length) {
return ( return (
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-4 text-center text-xs text-slate-500 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400"> <div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-4 text-center text-xs text-slate-500 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
Preview için en az bir sütun seçin. {translate('::App.VisualDesigner.Canvas.SelectAtLeastOneColumn')}
</div> </div>
) )
} }
@ -824,6 +836,7 @@ const SqlDataSourceView = ({
formScope: DesignerFormScope, formScope: DesignerFormScope,
) => React.ReactNode ) => React.ReactNode
}) => { }) => {
const { translate } = useLocalization()
const selectId = getSqlDataSourceEndpointId(node, 'selectEndpoint') const selectId = getSqlDataSourceEndpointId(node, 'selectEndpoint')
const keyField = getSqlDataSourceKeyField(node) const keyField = getSqlDataSourceKeyField(node)
const collectionPath = String(node.props.collectionPath ?? '') const collectionPath = String(node.props.collectionPath ?? '')
@ -872,7 +885,7 @@ const SqlDataSourceView = ({
const canDelete = Boolean(getSqlDataSourceEndpointId(node, 'deleteEndpoint')) const canDelete = Boolean(getSqlDataSourceEndpointId(node, 'deleteEndpoint'))
// Save follows the explicit mode, exactly like the generated runtime does. // Save follows the explicit mode, exactly like the generated runtime does.
const canSave = mode === 'new' ? canInsert : canUpdate const canSave = mode === 'new' ? canInsert : canUpdate
const designTimeTitle = 'Tasarım modunda endpoint çağrısı yapılmaz.' const designTimeTitle = translate('::App.VisualDesigner.Canvas.DesignTimeNoCall')
const toolbarButton = ( const toolbarButton = (
label: string, label: string,
@ -913,13 +926,21 @@ const SqlDataSourceView = ({
<span> <span>
key: <code>{keyField}</code> key: <code>{keyField}</code>
</span> </span>
<span>{selectId ? `${rows.length} kayıt` : 'Select endpointi seçilmedi'}</span> <span>
{selectId
? translate('::App.VisualDesigner.Canvas.RecordCount', { count: rows.length })
: translate('::App.VisualDesigner.Canvas.NoSelectEndpoint')}
</span>
<span className="rounded bg-sky-600 px-1.5 py-0.5 font-semibold text-white"> <span className="rounded bg-sky-600 px-1.5 py-0.5 font-semibold text-white">
{mode === 'new' ? 'Yeni kayıt' : 'Düzenleme'} {translate(
mode === 'new'
? '::App.VisualDesigner.Canvas.NewRecord'
: '::App.VisualDesigner.Canvas.Editing',
)}
</span> </span>
{rows.length > 1 && ( {rows.length > 1 && (
<label className="flex items-center gap-1"> <label className="flex items-center gap-1">
Satır {translate('::App.VisualDesigner.Canvas.Row')}
<select <select
className="rounded border border-sky-300 bg-white px-1 py-0.5 text-[10px] dark:border-sky-800 dark:bg-slate-900" className="rounded border border-sky-300 bg-white px-1 py-0.5 text-[10px] dark:border-sky-800 dark:bg-slate-900"
value={rowIndex} value={rowIndex}
@ -940,7 +961,7 @@ const SqlDataSourceView = ({
{/* The drop zone belongs with the content, above the command toolbar. */} {/* The drop zone belongs with the content, above the command toolbar. */}
{interactive && !node.children.length && ( {interactive && !node.children.length && (
<div className="rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400 dark:border-slate-700"> <div className="rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400 dark:border-slate-700">
Bileşeni buraya bırakın; Data sekmesinden sütununa bağlayın. {translate('::App.VisualDesigner.Canvas.DropAndBind')}
</div> </div>
)} )}
{node.props.showToolbar !== false && ( {node.props.showToolbar !== false && (
@ -948,46 +969,60 @@ const SqlDataSourceView = ({
{/* Navigation appears on its own once there is more than one record. */} {/* Navigation appears on its own once there is more than one record. */}
{rows.length > 1 && ( {rows.length > 1 && (
<> <>
{toolbarButton('Önceki', rowIndex > 0, 'plain', 'İlk kayıttasınız.', () => {toolbarButton(
goToRow(Math.max(0, rowIndex - 1)), translate('::App.VisualDesigner.Canvas.Previous'),
rowIndex > 0,
'plain',
translate('::App.VisualDesigner.Canvas.AtFirstRecord'),
() => goToRow(Math.max(0, rowIndex - 1)),
)} )}
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
{rows.length ? `${rowIndex + 1} / ${rows.length}` : '0 / 0'} {rows.length ? `${rowIndex + 1} / ${rows.length}` : '0 / 0'}
</span> </span>
{toolbarButton( {toolbarButton(
'Sonraki', translate('::App.VisualDesigner.Canvas.Next'),
rowIndex < rows.length - 1, rowIndex < rows.length - 1,
'plain', 'plain',
'Son kayıttasınız.', translate('::App.VisualDesigner.Canvas.AtLastRecord'),
() => goToRow(Math.min(rows.length - 1, rowIndex + 1)), () => goToRow(Math.min(rows.length - 1, rowIndex + 1)),
)} )}
<span className="mx-1 h-5 w-px bg-slate-300 dark:bg-slate-700" /> <span className="mx-1 h-5 w-px bg-slate-300 dark:bg-slate-700" />
</> </>
)} )}
{/* New and Reload only touch local state, so they work at design time. */} {/* New and Reload only touch local state, so they work at design time. */}
{toolbarButton('Yeni', canInsert, 'plain', 'Insert için POST endpointi seçin.', () => { {toolbarButton(
translate('::App.VisualDesigner.Canvas.New'),
canInsert,
'plain',
translate('::App.VisualDesigner.Canvas.SelectInsertEndpoint'),
() => {
setDraft({}) setDraft({})
setMode('new') setMode('new')
})} },
)}
{toolbarButton( {toolbarButton(
'Kaydet', translate('::Save'),
canSave, canSave,
'primary', 'primary',
translate(
mode === 'new' mode === 'new'
? 'Insert için POST endpointi seçin.' ? '::App.VisualDesigner.Canvas.SelectInsertEndpoint'
: 'Update için PUT endpointi seçin.', : '::App.VisualDesigner.Canvas.SelectUpdateEndpoint',
),
)} )}
{toolbarButton( {toolbarButton(
'Sil', translate('::App.Platform.Delete'),
canDelete && hasKey && mode === 'edit', canDelete && hasKey && mode === 'edit',
'danger', 'danger',
canDelete ? `Silmek için ${keyField} alanı dolu olmalıdır.` : 'DELETE endpointi seçin.', canDelete
? translate('::App.VisualDesigner.Canvas.KeyFieldRequired', { field: keyField })
: translate('::App.VisualDesigner.Canvas.SelectDeleteEndpoint'),
)} )}
{toolbarButton( {toolbarButton(
'Yenile', translate('::App.Platform.Refresh'),
Boolean(selectId), Boolean(selectId),
'plain', 'plain',
'Select için GET endpointi seçin.', translate('::App.VisualDesigner.Canvas.SelectSelectEndpoint'),
() => goToRow(rowIndex), () => goToRow(rowIndex),
)} )}
</div> </div>
@ -1014,7 +1049,9 @@ const resolveNodeContent = (
? getBindingValue(childrenBinding, dataValues, currentItem) ? getBindingValue(childrenBinding, dataValues, currentItem)
: undefined : undefined
if (boundChildren === null || boundChildren === undefined) { if (boundChildren === null || boundChildren === undefined) {
return (resolveStaticLanguageKeys(node.props.children, translate) as React.ReactNode) || undefined return (
(resolveStaticLanguageKeys(node.props.children, translate) as React.ReactNode) || undefined
)
} }
if (React.isValidElement(boundChildren)) return boundChildren if (React.isValidElement(boundChildren)) return boundChildren
return typeof boundChildren === 'object' ? JSON.stringify(boundChildren) : String(boundChildren) return typeof boundChildren === 'object' ? JSON.stringify(boundChildren) : String(boundChildren)
@ -1134,14 +1171,18 @@ const renderElement = (
const ownContent = resolveNodeContent(node, dataValues, currentItem, translate, refOverrides) const ownContent = resolveNodeContent(node, dataValues, currentItem, translate, refOverrides)
const hasOwnContent = ownContent !== undefined && ownContent !== null && ownContent !== '' const hasOwnContent = ownContent !== undefined && ownContent !== null && ownContent !== ''
const content: React.ReactNode = const content: React.ReactNode =
React.Children.count(children) > 0 React.Children.count(children) > 0 ? (
? hasOwnContent hasOwnContent ? (
? <> <>
{ownContent} {ownContent}
{children} {children}
</> </>
: children ) : (
: ownContent children
)
) : (
ownContent
)
const props = getPreviewProps( const props = getPreviewProps(
node, node,
dataValues, dataValues,
@ -1172,7 +1213,7 @@ const renderElement = (
key={String(option.value ?? index)} key={String(option.value ?? index)}
eventKey={String(option.value ?? index)} eventKey={String(option.value ?? index)}
> >
{String(option.label ?? option.value ?? `Menü ${index + 1}`)} {String(option.label ?? option.value ?? `#${index + 1}`)}
</UiKit.Menu.MenuItem> </UiKit.Menu.MenuItem>
))} ))}
{content} {content}
@ -1200,7 +1241,7 @@ const renderElement = (
disabled={Boolean(option.disabled)} disabled={Boolean(option.disabled)}
eventKey={String(option.value ?? index)} eventKey={String(option.value ?? index)}
> >
{String(option.label ?? option.value ?? `Öğe ${index + 1}`)} {String(option.label ?? option.value ?? `#${index + 1}`)}
</UiKit.Dropdown.Item> </UiKit.Dropdown.Item>
))} ))}
{content} {content}
@ -1226,7 +1267,7 @@ const renderElement = (
disabled={Boolean(option.disabled)} disabled={Boolean(option.disabled)}
value={option.value ?? index} value={option.value ?? index}
> >
{String(option.label ?? option.value ?? `Seçenek ${index + 1}`)} {String(option.label ?? option.value ?? `#${index + 1}`)}
</UiKit.Radio> </UiKit.Radio>
))} ))}
{content} {content}
@ -1348,7 +1389,7 @@ const NodeView = ({
const insideDraggedSubtree = React.useContext(DesignerDraggedSubtreeContext) || isDragSource const insideDraggedSubtree = React.useContext(DesignerDraggedSubtreeContext) || isDragSource
const canDropInside = acceptsDroppedChildren && !insideDraggedSubtree const canDropInside = acceptsDroppedChildren && !insideDraggedSubtree
const canDropBeside = !insideDraggedSubtree const canDropBeside = !insideDraggedSubtree
const dragLabel = drag?.label || drag?.name || 'Bileşen' const dragLabel = drag?.label || drag?.name || translate('::App.VisualDesigner.Canvas.Component')
/** /**
* Taşımayı başlatan ortak kod. Hem düğüm gövdesi hem de tutamak bunu kullanır: * Taşımayı başlatan ortak kod. Hem düğüm gövdesi hem de tutamak bunu kullanır:
* Input/Select/Textarea gibi canlı önizleme kontrolleri Chrome'da üst öğenin * Input/Select/Textarea gibi canlı önizleme kontrolleri Chrome'da üst öğenin
@ -1500,7 +1541,7 @@ const NodeView = ({
disabled={Boolean(option.disabled)} disabled={Boolean(option.disabled)}
value={String(option.value ?? index)} value={String(option.value ?? index)}
> >
{String(option.label ?? option.value ?? `Sekme ${index + 1}`)} {String(option.label ?? option.value ?? `#${index + 1}`)}
</UiKit.Tabs.TabNav> </UiKit.Tabs.TabNav>
))} ))}
</UiKit.Tabs.TabList> </UiKit.Tabs.TabList>
@ -1560,7 +1601,7 @@ const NodeView = ({
))} ))}
{interactive && !tabNodes.length && !option.content && ( {interactive && !tabNodes.length && !option.content && (
<div className="flex min-h-12 items-center justify-center rounded border border-dashed border-slate-300 px-2 text-center text-[10px] text-slate-400 dark:border-slate-700"> <div className="flex min-h-12 items-center justify-center rounded border border-dashed border-slate-300 px-2 text-center text-[10px] text-slate-400 dark:border-slate-700">
Bu sekme için komponent bırakın {translate('::App.VisualDesigner.Canvas.DropComponentInTab')}
</div> </div>
)} )}
</div> </div>
@ -1655,7 +1696,7 @@ const NodeView = ({
))} ))}
{interactive && !cellNodes.length && ( {interactive && !cellNodes.length && (
<div className="flex min-h-12 items-center justify-center rounded border border-dashed border-slate-300 px-2 text-center text-[10px] text-slate-400 dark:border-slate-700"> <div className="flex min-h-12 items-center justify-center rounded border border-dashed border-slate-300 px-2 text-center text-[10px] text-slate-400 dark:border-slate-700">
Komponent bırakın {translate('::App.VisualDesigner.Canvas.DropComponent')}
</div> </div>
)} )}
</div> </div>
@ -1714,7 +1755,7 @@ const NodeView = ({
key={`grid_loading_${node.id}`} key={`grid_loading_${node.id}`}
className="rounded-lg border border-slate-200 bg-slate-50 p-4 text-center text-xs text-slate-500 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400" className="rounded-lg border border-slate-200 bg-slate-50 p-4 text-center text-xs text-slate-500 [grid-column:1/-1] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400"
> >
Veriler yükleniyor {translate('::App.VisualDesigner.Canvas.LoadingData')}
</div>, </div>,
] ]
: node.type === 'Grid' && : node.type === 'Grid' &&
@ -1775,7 +1816,9 @@ const NodeView = ({
} ${hasStaticChildren || hasChildrenBinding ? 'mt-3' : ''}`} } ${hasStaticChildren || hasChildrenBinding ? 'mt-3' : ''}`}
{...containerDropZoneProps} {...containerDropZoneProps}
> >
{containerDropZone.over ? `${dragLabel} buraya bırakılacak` : 'Bileşeni buraya bırakın'} {containerDropZone.over
? translate('::App.VisualDesigner.Canvas.WillDropHere', { label: dragLabel })
: translate('::App.VisualDesigner.Canvas.DropComponentHere')}
</div>, </div>,
] ]
: renderedChildren : renderedChildren
@ -1880,7 +1923,7 @@ const NodeView = ({
{/* Sürükleme sürerken bırakılamayan hedef açıkça söylenir. */} {/* Sürükleme sürerken bırakılamayan hedef açıkça söylenir. */}
{dragging && insideDraggedSubtree && !isDragSource && ( {dragging && insideDraggedSubtree && !isDragSource && (
<span className="pointer-events-none absolute right-1 top-1 z-30 rounded bg-slate-500 px-1.5 py-[1px] text-[10px] font-semibold text-white shadow"> <span className="pointer-events-none absolute right-1 top-1 z-30 rounded bg-slate-500 px-1.5 py-[1px] text-[10px] font-semibold text-white shadow">
buraya taşınamaz {translate('::App.VisualDesigner.Canvas.CannotMoveHere')}
</span> </span>
)} )}
{interactive && ( {interactive && (
@ -1895,7 +1938,9 @@ const NodeView = ({
<span <span
draggable draggable
className="flex cursor-grab items-center gap-1 px-2 text-[10px] font-semibold active:cursor-grabbing" className="flex cursor-grab items-center gap-1 px-2 text-[10px] font-semibold active:cursor-grabbing"
title={`${node.ref || node.type} — sürükleyerek taşı`} title={translate('::App.VisualDesigner.Canvas.DragToMove', {
name: node.ref || node.type,
})}
onDragStart={startNodeDrag} onDragStart={startNodeDrag}
onDragEnd={() => endDesignerDrag()} onDragEnd={() => endDesignerDrag()}
> >
@ -1904,7 +1949,9 @@ const NodeView = ({
<button <button
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40" className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
disabled={!canMoveUp} disabled={!canMoveUp}
title={canMoveUp ? 'Yukarı taşı' : 'Zaten en üstte'} title={translate(
canMoveUp ? '::App.Platform.MoveUp' : '::App.VisualDesigner.Canvas.AlreadyAtTop',
)}
type="button" type="button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
@ -1916,7 +1963,11 @@ const NodeView = ({
<button <button
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40" className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
disabled={!canMoveDown} disabled={!canMoveDown}
title={canMoveDown ? 'Aşağı taşı' : 'Zaten en altta'} title={translate(
canMoveDown
? '::App.Platform.MoveDown'
: '::App.VisualDesigner.Canvas.AlreadyAtBottom',
)}
type="button" type="button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
@ -1927,7 +1978,7 @@ const NodeView = ({
</button> </button>
<button <button
className="p-1.5 hover:bg-sky-700" className="p-1.5 hover:bg-sky-700"
title="Çoğalt" title={translate('::App.VisualDesigner.Canvas.Duplicate')}
type="button" type="button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
@ -1938,7 +1989,7 @@ const NodeView = ({
</button> </button>
<button <button
className="p-1.5 hover:bg-red-600" className="p-1.5 hover:bg-red-600"
title="Sil" title={translate('::App.Platform.Delete')}
type="button" type="button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
@ -1984,7 +2035,7 @@ const NodeView = ({
className="m-2 rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400" className="m-2 rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400"
{...containerDropZoneProps} {...containerDropZoneProps}
> >
Bileşeni buraya bırakın {translate('::App.VisualDesigner.Canvas.DropComponentHere')}
</div> </div>
)} )}
<span className="hidden">{index}</span> <span className="hidden">{index}</span>
@ -2009,6 +2060,7 @@ const VisualCanvas = ({
renderCustomComponent, renderCustomComponent,
dataValues = {}, dataValues = {},
}: VisualCanvasProps) => { }: VisualCanvasProps) => {
const { translate } = useLocalization()
const previewDataValues = dataValues const previewDataValues = dataValues
const [refState, setRefState] = React.useState<Record<string, DesignerRefOverride>>({}) const [refState, setRefState] = React.useState<Record<string, DesignerRefOverride>>({})
const nodesByRef = React.useMemo(() => { const nodesByRef = React.useMemo(() => {
@ -2080,7 +2132,8 @@ const VisualCanvas = ({
event.preventDefault() event.preventDefault()
endDesignerDrag() endDesignerDrag()
const raw = const raw =
event.dataTransfer.getData(DESIGNER_DRAG_TYPE) || event.dataTransfer.getData('text/plain') event.dataTransfer.getData(DESIGNER_DRAG_TYPE) ||
event.dataTransfer.getData('text/plain')
if (!raw) return if (!raw) return
const payload = JSON.parse(raw) const payload = JSON.parse(raw)
if (payload.source === 'library') { if (payload.source === 'library') {
@ -2094,8 +2147,8 @@ const VisualCanvas = ({
{drag && interactive && ( {drag && interactive && (
<div className="pointer-events-none sticky top-0 z-40 mb-3 flex items-center justify-center"> <div className="pointer-events-none sticky top-0 z-40 mb-3 flex items-center justify-center">
<span className="rounded-full bg-slate-900/90 px-3 py-1 text-[11px] font-medium text-white shadow-lg"> <span className="rounded-full bg-slate-900/90 px-3 py-1 text-[11px] font-medium text-white shadow-lg">
<b>{drag.label || drag.name}</b> sürükleniyor · kesikli çerçeveli alanlar içine alır, <b>{drag.label || drag.name}</b>{' '}
yeşil çizgi araya yerleştirir {translate('::App.VisualDesigner.Canvas.DraggingHint')}
</span> </span>
</div> </div>
)} )}
@ -2134,7 +2187,7 @@ const VisualCanvas = ({
}`} }`}
{...rootDropZone.props} {...rootDropZone.props}
> >
Sayfanın en altına bırak {translate('::App.VisualDesigner.Canvas.DropAtPageEnd')}
</div> </div>
)} )}
</div> </div>
@ -2154,19 +2207,22 @@ const VisualCanvas = ({
+ +
</div> </div>
<h3 className="font-semibold text-slate-700 dark:text-slate-200"> <h3 className="font-semibold text-slate-700 dark:text-slate-200">
Sayfa layoutunu seçin {translate('::App.VisualDesigner.Canvas.ChoosePageLayout')}
</h3> </h3>
<p className="mt-1 text-sm text-slate-500"> <p className="mt-1 text-sm text-slate-500">
Hazır bir yerleşimle başlayın veya Toolboxtaki My Components grubundan kendi layout {translate('::App.VisualDesigner.Canvas.ChoosePageLayoutHint')}
komponentinizi kullanın.
</p> </p>
{interactive && ( {interactive && (
<div className="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-4"> <div className="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-4">
{[ {[
['PageContainer', 'Sayfa alanı', '□'], ['PageContainer', translate('::App.VisualDesigner.Layout.PageContainer'), '□'],
['TwoColumns', 'İki kolon', '▥'], ['TwoColumns', translate('::App.VisualDesigner.Layout.TwoColumns'), '▥'],
['SidebarContent', 'Sidebar + içerik', '◧'], [
['HeaderContent', 'Üst alan + içerik', '⊟'], 'SidebarContent',
translate('::App.VisualDesigner.Layout.SidebarContent'),
'◧',
],
['HeaderContent', translate('::App.VisualDesigner.Layout.HeaderContent'), '⊟'],
].map(([name, label, icon]) => ( ].map(([name, label, icon]) => (
<button <button
key={name} key={name}

View file

@ -187,93 +187,93 @@ export interface DesignerScriptRecipe {
} }
export const DESIGNER_SCRIPT_RECIPE_GROUPS = [ export const DESIGNER_SCRIPT_RECIPE_GROUPS = [
'Komponent erişimi', 'App.VisualDesigner.Catalog.Group.ComponentAccess',
'API çağrıları', 'App.VisualDesigner.Catalog.Group.ApiCalls',
'SqlDataSource', 'App.VisualDesigner.Catalog.Group.SqlDataSource',
'Event ve sayfa', 'App.VisualDesigner.Catalog.Group.EventAndPage',
] as const ] as const
export const DESIGNER_SCRIPT_RECIPES: DesignerScriptRecipe[] = [ export const DESIGNER_SCRIPT_RECIPES: DesignerScriptRecipe[] = [
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Değer atama ve okuma', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title',
description: 'Her komponent kendi ref adıyla adreslenir.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description',
code: `refs.{{ref}}.setValue('Merhaba') code: `refs.{{ref}}.setValue('Merhaba')
const value = refs.{{ref}}.getValue() const value = refs.{{ref}}.getValue()
console.log(value)`, console.log(value)`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Aktif / pasif yapma', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title2',
description: 'Kaydet butonunu koşula göre kilitlemek için.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description2',
code: `refs.{{ref}}.setEnabled(false) code: `refs.{{ref}}.setEnabled(false)
console.log(refs.{{ref}}.isEnabled())`, console.log(refs.{{ref}}.isEnabled())`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Gösterme / gizleme', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title3',
description: 'Gizlenen komponent sayfada hiç render edilmez.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description3',
code: `refs.{{ref}}.setVisible(false) code: `refs.{{ref}}.setVisible(false)
// veya: refs.{{ref}}.hide() / refs.{{ref}}.show()`, // veya: refs.{{ref}}.hide() / refs.{{ref}}.show()`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Salt okunur yapma', title: 'Salt okunur yapma',
description: 'Değeri görünür kalsın ama düzenlenemesin.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description4',
code: `refs.{{ref}}.setReadOnly(true)`, code: `refs.{{ref}}.setReadOnly(true)`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Etiket / metin değiştirme', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title4',
description: 'Buton ve etiket yazısını çalışma anında değiştirir.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description5',
code: `refs.{{ref}}.setText('Kaydediliyor…')`, code: `refs.{{ref}}.setText('Kaydediliyor…')`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Serbest property atama', title: 'Serbest property atama',
description: 'Property panelindeki her property script ile de yazılabilir.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description6',
code: `refs.{{ref}}.setProps({ placeholder: 'Ara…', className: 'border-red-500' }) code: `refs.{{ref}}.setProps({ placeholder: 'Ara…', className: 'border-red-500' })
refs.{{ref}}.setProp('size', 'sm')`, refs.{{ref}}.setProp('size', 'sm')`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Script ile yapılanları geri alma', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title5',
description: 'Komponenti tasarımdaki haline döndürür.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description7',
code: `refs.{{ref}}.reset()`, code: `refs.{{ref}}.reset()`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'GET — kayıt okuma', title: 'App.VisualDesigner.Catalog.ApiCalls.Title',
description: 'Sonuç doğrudan response bodysidir.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description',
code: `const rows = await api.get('{{url}}') code: `const rows = await api.get('{{url}}')
console.log(rows)`, console.log(rows)`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'GET — parametre ile', title: 'App.VisualDesigner.Catalog.ApiCalls.Title2',
description: 'İkinci argüman query string olarak gönderilir.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description2',
code: `const rows = await api.get('{{url}}', { maxResultCount: 10 }) code: `const rows = await api.get('{{url}}', { maxResultCount: 10 })
console.log(rows)`, console.log(rows)`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'POST — yeni kayıt', title: 'App.VisualDesigner.Catalog.ApiCalls.Title3',
description: 'Gövde ikinci argüman olarak verilir.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description3',
code: `const created = await api.post('{{url}}', { code: `const created = await api.post('{{url}}', {
name: refs.{{ref}}.getValue(), name: refs.{{ref}}.getValue(),
}) })
console.log(created)`, console.log(created)`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'PUT ve DELETE', title: 'App.VisualDesigner.Catalog.ApiCalls.Title4',
description: 'Güncelleme ve silme aynı imzayı kullanır.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description4',
code: `await api.put('{{url}}/5', { name: 'Yeni ad' }) code: `await api.put('{{url}}/5', { name: 'Yeni ad' })
await api.delete('{{url}}/5')`, await api.delete('{{url}}/5')`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'Hata yönetimi ve bildirim', title: 'App.VisualDesigner.Catalog.ApiCalls.Title5',
description: 'notify tipleri: success, info, warning, danger.', description: 'notify tipleri: success, info, warning, danger.',
code: `try { code: `try {
await api.post('{{url}}', { name: refs.{{ref}}.getValue() }) await api.post('{{url}}', { name: refs.{{ref}}.getValue() })
@ -283,9 +283,9 @@ await api.delete('{{url}}/5')`,
}`, }`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'Çağrı boyunca butonu kilitleme', title: 'App.VisualDesigner.Catalog.ApiCalls.Title6',
description: 'Çift tıklamayı ve mükerrer isteği önler.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description5',
code: `refs.{{ref}}.setEnabled(false) code: `refs.{{ref}}.setEnabled(false)
refs.{{ref}}.setText('Gönderiliyor…') refs.{{ref}}.setText('Gönderiliyor…')
try { try {
@ -299,9 +299,9 @@ try {
}`, }`,
}, },
{ {
group: 'API çağrıları', group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
title: 'Serbest istek', title: 'Serbest istek',
description: 'Header, timeout gibi ayarlar gerektiğinde.', description: 'App.VisualDesigner.Catalog.ApiCalls.Description6',
code: `const result = await api.request({ code: `const result = await api.request({
url: '{{url}}', url: '{{url}}',
method: 'GET', method: 'GET',
@ -310,26 +310,25 @@ try {
console.log(result)`, console.log(result)`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Selectin görünmeyen sütunlarını okuma', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title6',
description: description: 'App.VisualDesigner.Catalog.SelectHiddenColumns.Description',
'Select yalnızca Label gösterir; Data sekmesinde eklenen sütunlar seçenekle birlikte taşınır.',
code: `// onChange içinde: seçilen kaydın diğer sütunları code: `// onChange içinde: seçilen kaydın diğer sütunları
const fiyat = refs.{{ref}}.getColumn('unitPrice') const fiyat = refs.{{ref}}.getColumn('unitPrice')
const stok = refs.{{ref}}.getColumn('stockQuantity') const stok = refs.{{ref}}.getColumn('stockQuantity')
refs.{{sql}}.setFields({ UnitPrice: fiyat, Stock: stok })`, refs.{{sql}}.setFields({ UnitPrice: fiyat, Stock: stok })`,
}, },
{ {
group: 'Komponent erişimi', group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
title: 'Seçilen kaydın tamamı', title: 'App.VisualDesigner.Catalog.ComponentAccess.Title7',
description: 'Çoklu seçimde dizi döner.', description: 'App.VisualDesigner.Catalog.ComponentAccess.Description8',
code: `const secilen = refs.{{ref}}.getSelectedOption() code: `const secilen = refs.{{ref}}.getSelectedOption()
console.log(secilen, refs.{{ref}}.getLabel())`, console.log(secilen, refs.{{ref}}.getLabel())`,
}, },
{ {
group: 'SqlDataSource', group: 'SqlDataSource',
title: 'Kaydet / sil / yenile', title: 'App.VisualDesigner.Catalog.SqlDataSource.Title',
description: 'Toolbarı gizleyip kendi butonlarınızı kullanabilirsiniz.', description: 'App.VisualDesigner.Catalog.SqlDataSource.Description',
code: `await refs.{{sql}}.save() code: `await refs.{{sql}}.save()
// await refs.{{sql}}.remove() // await refs.{{sql}}.remove()
// await refs.{{sql}}.reload() // await refs.{{sql}}.reload()
@ -337,23 +336,23 @@ console.log(secilen, refs.{{ref}}.getLabel())`,
}, },
{ {
group: 'SqlDataSource', group: 'SqlDataSource',
title: 'Aktif kaydı okuma ve yazma', title: 'App.VisualDesigner.Catalog.SqlDataSource.Title2',
description: 'Alan adı Select sonucundaki sütun adıdır.', description: 'App.VisualDesigner.Catalog.SqlDataSource.Description2',
code: `const record = refs.{{sql}}.getRecord() code: `const record = refs.{{sql}}.getRecord()
console.log(record) console.log(record)
refs.{{sql}}.setField('name', 'Yeni ad')`, refs.{{sql}}.setField('name', 'Yeni ad')`,
}, },
{ {
group: 'SqlDataSource', group: 'SqlDataSource',
title: 'Kayıtlar arasında gezinme', title: 'App.VisualDesigner.Catalog.SqlDataSource.Title3',
description: 'Select birden fazla satır döndürdüğünde.', description: 'App.VisualDesigner.Catalog.SqlDataSource.Description3',
code: `const rows = refs.{{sql}}.getRows() code: `const rows = refs.{{sql}}.getRows()
refs.{{sql}}.goToRow(rows.length - 1)`, refs.{{sql}}.goToRow(rows.length - 1)`,
}, },
{ {
group: 'SqlDataSource', group: 'SqlDataSource',
title: 'Kaydetmeden önce doğrulama', title: 'App.VisualDesigner.Catalog.SqlDataSource.Title4',
description: 'Zorunlu alan boşsa işlemi durdurur.', description: 'App.VisualDesigner.Catalog.SqlDataSource.Description4',
code: `const record = refs.{{sql}}.getRecord() code: `const record = refs.{{sql}}.getRecord()
if (!record.name) { if (!record.name) {
notify('Ad alanı zorunludur', 'warning') notify('Ad alanı zorunludur', 'warning')
@ -363,35 +362,35 @@ await refs.{{sql}}.save()`,
}, },
{ {
group: 'SqlDataSource', group: 'SqlDataSource',
title: 'Durum bilgisi', title: 'App.VisualDesigner.Catalog.SqlDataSource.Title5',
description: 'Yeni kayıt / düzenleme, meşgul ve hata durumu.', description: 'App.VisualDesigner.Catalog.SqlDataSource.Description5',
code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`, code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`,
}, },
{ {
group: 'Event ve sayfa', group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
title: 'Event verisi', title: 'Event verisi',
description: 'event içeriği komponent tipine göre değişir.', description: 'App.VisualDesigner.Catalog.EventAndPage.Description',
code: `// Input: event.target.value · Checkbox: event.checked code: `// Input: event.target.value · Checkbox: event.checked
// Select/Dropdown/Menu: seçilen değer · Button: tıklama eventi // Select/Dropdown/Menu: seçilen değer · Button: tıklama eventi
console.log(event)`, console.log(event)`,
}, },
{ {
group: 'Event ve sayfa', group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
title: 'Komponentin kendi bilgileri', title: 'Komponentin kendi bilgileri',
description: 'Scriptin bağlı olduğu komponent ve tasarım propertyleri.', description: 'App.VisualDesigner.Catalog.EventAndPage.Description2',
code: `console.log(component.type, component.ref) code: `console.log(component.type, component.ref)
console.log(props)`, console.log(props)`,
}, },
{ {
group: 'Event ve sayfa', group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
title: 'Sayfa yönlendirme', title: 'App.VisualDesigner.Catalog.EventAndPage.Title',
description: 'Kayıt sonrası liste sayfasına dönmek için.', description: 'App.VisualDesigner.Catalog.EventAndPage.Description3',
code: `window.location.href = '/app/orders'`, code: `window.location.href = '/app/orders'`,
}, },
{ {
group: 'Event ve sayfa', group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
title: 'URL parametresi okuma', title: 'URL parametresi okuma',
description: 'Detay sayfasında anahtar değerini almak için.', description: 'App.VisualDesigner.Catalog.EventAndPage.Description4',
code: `const id = new URLSearchParams(window.location.search).get('id') code: `const id = new URLSearchParams(window.location.search).get('id')
console.log(id)`, console.log(id)`,
}, },
@ -405,7 +404,7 @@ const COLLECTION_ITEMS_PROPERTY = {
type: 'array' as const, type: 'array' as const,
value: [], value: [],
category: 'properties' as const, category: 'properties' as const,
description: 'Statik veya endpoint kaynaklı veri koleksiyonu', description: 'App.VisualDesigner.Catalog.Items.Description',
tsType: 'unknown[]', tsType: 'unknown[]',
options: undefined, options: undefined,
required: false, required: false,
@ -415,7 +414,7 @@ const GRID_COMPACT_PROPERTY = {
type: 'boolean' as const, type: 'boolean' as const,
value: false, value: false,
category: 'properties' as const, category: 'properties' as const,
description: 'Satır ve hücre boşluklarını azaltır', description: 'App.VisualDesigner.Catalog.Compact.Description',
tsType: 'boolean', tsType: 'boolean',
options: undefined, options: undefined,
required: false, required: false,
@ -426,7 +425,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
type: 'boolean' as const, type: 'boolean' as const,
value: true, value: true,
category: 'properties' as const, category: 'properties' as const,
description: 'Satırların üzerine gelindiğinde vurgu rengi gösterir', description: 'App.VisualDesigner.Catalog.Hoverable.Description',
tsType: 'boolean', tsType: 'boolean',
options: undefined, options: undefined,
required: false, required: false,
@ -436,7 +435,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
type: 'boolean' as const, type: 'boolean' as const,
value: true, value: true,
category: 'properties' as const, category: 'properties' as const,
description: 'Geniş tablolarda yatay kaydırmayı etkinleştirir', description: 'App.VisualDesigner.Catalog.Overflow.Description',
tsType: 'boolean', tsType: 'boolean',
options: undefined, options: undefined,
required: false, required: false,
@ -446,7 +445,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
type: 'boolean' as const, type: 'boolean' as const,
value: false, value: false,
category: 'properties' as const, category: 'properties' as const,
description: 'Veri satırları arasındaki kenarlıkları kaldırır', description: 'App.VisualDesigner.Catalog.BorderlessRow.Description',
tsType: 'boolean', tsType: 'boolean',
options: undefined, options: undefined,
required: false, required: false,
@ -458,7 +457,7 @@ const TABLE_DIMENSION_PROPERTIES = [
type: 'number' as const, type: 'number' as const,
value: 2, value: 2,
category: 'properties' as const, category: 'properties' as const,
description: 'Table içindeki statik satır sayısı', description: 'App.VisualDesigner.Catalog.RowCount.Description',
tsType: 'number', tsType: 'number',
options: undefined, options: undefined,
required: false, required: false,
@ -468,7 +467,7 @@ const TABLE_DIMENSION_PROPERTIES = [
type: 'number' as const, type: 'number' as const,
value: 3, value: 3,
category: 'properties' as const, category: 'properties' as const,
description: 'Her satırdaki statik hücre sayısı', description: 'App.VisualDesigner.Catalog.ColumnCount.Description',
tsType: 'number', tsType: 'number',
options: undefined, options: undefined,
required: false, required: false,
@ -478,11 +477,11 @@ const SELECT_OPTIONS_PROPERTY = {
name: 'options', name: 'options',
type: 'array' as const, type: 'array' as const,
value: [ value: [
{ label: 'Seçenek 1', value: '1' }, { label: 'App.VisualDesigner.Catalog.Options.Label', value: '1' },
{ label: 'Seçenek 2', value: '2' }, { label: 'App.VisualDesigner.Catalog.Options.Label2', value: '2' },
], ],
category: 'properties' as const, category: 'properties' as const,
description: 'Select items; statik olarak veya endpoint koleksiyonundan beslenebilir', description: 'App.VisualDesigner.Catalog.Options.Description',
tsType: 'Array<{ label: string; value: unknown }>', tsType: 'Array<{ label: string; value: unknown }>',
options: undefined, options: undefined,
required: false, required: false,
@ -490,7 +489,7 @@ const SELECT_OPTIONS_PROPERTY = {
const MENU_ITEMS_PROPERTY = { const MENU_ITEMS_PROPERTY = {
...SELECT_OPTIONS_PROPERTY, ...SELECT_OPTIONS_PROPERTY,
name: 'items', name: 'items',
description: 'Menu items; statik olarak veya endpoint koleksiyonundan beslenebilir', description: 'App.VisualDesigner.Catalog.Items.Description2',
} }
const TABS_ITEMS_PROPERTY = { const TABS_ITEMS_PROPERTY = {
...SELECT_OPTIONS_PROPERTY, ...SELECT_OPTIONS_PROPERTY,
@ -499,8 +498,7 @@ const TABS_ITEMS_PROPERTY = {
{ label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' }, { label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' },
{ label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' }, { label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' },
], ],
description: description: 'App.VisualDesigner.Catalog.Tabs.Description',
'Sekmeler; statik olarak veya endpoint koleksiyonundan beslenebilir. content alanı sekme içeriğini yazar.',
tsType: 'Array<{ label: string; value: unknown; content?: string }>', tsType: 'Array<{ label: string; value: unknown; content?: string }>',
} }
@ -723,19 +721,24 @@ const platformDefinition = (
}) })
export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [ export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [
platformDefinition('ListView', 'List', 'App.VisualDesigner.Catalog.ListView.Description'),
platformDefinition('DataGridView', 'Grid', 'App.VisualDesigner.Catalog.DataGridView.Description'),
platformDefinition('TreeView', 'Tree', 'App.VisualDesigner.Catalog.TreeView.Description'),
platformDefinition('GanttView', 'GanttView', 'App.VisualDesigner.Catalog.GanttView.Description'),
platformDefinition('TodoBoard', 'TodoBoard', 'App.VisualDesigner.Catalog.TodoBoard.Description'),
platformDefinition('CardView', 'CardView', 'App.VisualDesigner.Catalog.CardView.Description'),
platformDefinition( platformDefinition(
'ListView', 'SchedulerView',
'List', 'SchedulerView',
'Tüm etkin platform görünümlerini /admin/list/:listFormCode ile aynı şekilde yönetir', 'App.VisualDesigner.Catalog.SchedulerView.Description',
),
platformDefinition('PivotView', 'Pivot', 'App.VisualDesigner.Catalog.PivotView.Description'),
platformDefinition(
'ChartView',
'Chart',
'App.VisualDesigner.Catalog.ChartView.Description',
'420px',
), ),
platformDefinition('DataGridView', 'Grid', 'Sozsoft dinamik veri grid görünümü'),
platformDefinition('TreeView', 'Tree', 'Sozsoft dinamik ağaç görünümü'),
platformDefinition('GanttView', 'GanttView', 'Sozsoft dinamik Gantt görünümü'),
platformDefinition('TodoBoard', 'TodoBoard', 'Sozsoft dinamik iş panosu'),
platformDefinition('CardView', 'CardView', 'Sozsoft dinamik kart görünümü'),
platformDefinition('SchedulerView', 'SchedulerView', 'Sozsoft dinamik planlayıcı görünümü'),
platformDefinition('PivotView', 'Pivot', 'Sozsoft dinamik pivot görünümü'),
platformDefinition('ChartView', 'Chart', 'Sozsoft dinamik grafik görünümü', '420px'),
] ]
/** /**
@ -750,8 +753,7 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
category: 'data', category: 'data',
kind: 'layout', kind: 'layout',
toolboxGroup: 'data', toolboxGroup: 'data',
description: description: 'App.VisualDesigner.Catalog.SqlDataSource.Description',
'GET/POST/PUT/DELETE endpointlerini tek kayıt üzerinde yöneten veri kabı; içine bırakılan komponentler sütunlara bağlanır',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ {
@ -759,42 +761,42 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'Kaydı okuyan GET endpointinin data source idsi (Data sekmesinden seçilir)', description: 'App.VisualDesigner.Catalog.String.Description',
}, },
{ {
name: 'insertEndpoint', name: 'insertEndpoint',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'Yeni kayıt için POST endpointi', description: 'App.VisualDesigner.Catalog.String.Description2',
}, },
{ {
name: 'updateEndpoint', name: 'updateEndpoint',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'Mevcut kaydı güncelleyen PUT endpointi', description: 'App.VisualDesigner.Catalog.String.Description3',
}, },
{ {
name: 'deleteEndpoint', name: 'deleteEndpoint',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'Kaydı silen DELETE endpointi', description: 'App.VisualDesigner.Catalog.String.Description4',
}, },
{ {
name: 'keyFieldName', name: 'keyFieldName',
type: 'string', type: 'string',
value: 'id', value: 'id',
category: 'properties', category: 'properties',
description: 'Insert/Update ayrımı ve endpoint parametreleri bu sütundan doldurulur', description: 'App.VisualDesigner.Catalog.String.Description5',
}, },
{ {
name: 'collectionPath', name: 'collectionPath',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'GET cevabı içinde satırların bulunduğu path (boşsa cevabın kendisi kullanılır)', description: 'App.VisualDesigner.Catalog.String.Description6',
}, },
{ {
name: 'keySource', name: 'keySource',
@ -802,36 +804,35 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
value: 'query', value: 'query',
options: ['query', 'route'], options: ['query', 'route'],
category: 'properties', category: 'properties',
description: 'Key değerinin sayfa URLsinden okunma şekli', description: 'App.VisualDesigner.Catalog.Select.Description',
}, },
{ {
name: 'keyParamName', name: 'keyParamName',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: 'URLden okunacak parametre adı (boşsa key field kullanılır)', description: 'App.VisualDesigner.Catalog.String.Description7',
}, },
{ {
name: 'previewKeyValue', name: 'previewKeyValue',
type: 'string', type: 'string',
value: '', value: '',
category: 'properties', category: 'properties',
description: description: 'App.VisualDesigner.Catalog.PreviewKeyValue.Description',
'Sadece tasarım ekranı: GetById endpointinin sütunlarını okuyabilmek için örnek key değeri',
}, },
{ {
name: 'autoLoad', name: 'autoLoad',
type: 'boolean', type: 'boolean',
value: true, value: true,
category: 'properties', category: 'properties',
description: 'Sayfa açıldığında Select endpointini otomatik çağırır', description: 'App.VisualDesigner.Catalog.Boolean.Description',
}, },
{ {
name: 'showToolbar', name: 'showToolbar',
type: 'boolean', type: 'boolean',
value: true, value: true,
category: 'properties', category: 'properties',
description: 'New / Save / Delete / Reload butonlarını gösterir', description: 'App.VisualDesigner.Catalog.Boolean.Description2',
}, },
{ {
name: 'gap', name: 'gap',
@ -861,7 +862,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'layout', toolboxGroup: 'layout',
description: 'Intranet içeriği için ortalanmış sayfa alanı', description: 'App.VisualDesigner.Catalog.PageContainer.Description',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ name: 'maxWidth', type: 'string', value: '1280px', category: 'styling' }, { name: 'maxWidth', type: 'string', value: '1280px', category: 'styling' },
@ -877,7 +878,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'layout', toolboxGroup: 'layout',
description: 'Sayfayı iki eşit kolona böler', description: 'App.VisualDesigner.Catalog.TwoColumns.Description',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ name: 'gap', type: 'number', value: 16, category: 'styling' }, { name: 'gap', type: 'number', value: 16, category: 'styling' },
@ -891,7 +892,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'layout', toolboxGroup: 'layout',
description: 'Dar sidebar ve geniş içerik alanı', description: 'App.VisualDesigner.Catalog.SidebarContent.Description',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ name: 'sidebarWidth', type: 'string', value: '280px', category: 'styling' }, { name: 'sidebarWidth', type: 'string', value: '280px', category: 'styling' },
@ -906,7 +907,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'layout', toolboxGroup: 'layout',
description: 'Üst alan ve alt içerik için dikey yerleşim', description: 'App.VisualDesigner.Catalog.HeaderContent.Description',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ name: 'gap', type: 'number', value: 16, category: 'styling' }, { name: 'gap', type: 'number', value: 16, category: 'styling' },
@ -920,7 +921,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'layout', toolboxGroup: 'layout',
description: 'Yatay ve responsive yerleşim', description: 'App.VisualDesigner.Catalog.FlexRow.Description',
acceptsChildren: true, acceptsChildren: true,
properties: [ properties: [
{ name: 'gap', type: 'number', value: 16, category: 'styling' }, { name: 'gap', type: 'number', value: 16, category: 'styling' },
@ -942,7 +943,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
category: 'layout', category: 'layout',
kind: 'layout', kind: 'layout',
toolboxGroup: 'ui', toolboxGroup: 'ui',
description: 'Bileşenler arasında boşluk', description: 'App.VisualDesigner.Catalog.Spacer.Description',
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }], properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
hooks: [], hooks: [],
}, },
@ -987,7 +988,7 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
category: 'custom', category: 'custom',
kind: 'custom', kind: 'custom',
toolboxGroup: 'custom', toolboxGroup: 'custom',
description: 'Veritabanındaki yeniden kullanılabilir bileşen', description: 'App.VisualDesigner.Catalog.Spacer.Description2',
acceptsChildren: true, acceptsChildren: true,
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }], properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
hooks: [], hooks: [],

View file

@ -32,6 +32,7 @@ import {
type DesignerRuleCondition, type DesignerRuleCondition,
type DesignerScriptContext, type DesignerScriptContext,
type DesignerScriptRule, type DesignerScriptRule,
type TranslateFn,
} from './designerScriptRecipes' } from './designerScriptRecipes'
import type { SqlDataSourceEvent } from './types' import type { SqlDataSourceEvent } from './types'
@ -75,57 +76,57 @@ const snippets = (sqlRef: string): ScriptSnippet[] => {
const sql = sqlRef || 'sqlDataSource1' const sql = sqlRef || 'sqlDataSource1'
return [ return [
{ {
label: 'Alan değişimine göre hesapla', label: 'App.VisualDesigner.Snippet.CalcOnFieldChange.Label',
group: 'Kayıt', group: 'App.VisualDesigner.Snippet.Group.Record',
description: 'Sadece ilgili sütunlar değişince hesaplar.', description: 'App.VisualDesigner.Snippet.CalcOnFieldChange.Description',
code: `if (['\${1:Quantity}', '\${2:UnitPrice}'].includes(event.field)) {\n const total = Number(event.record.\${1:Quantity} || 0) * Number(event.record.\${2:UnitPrice} || 0)\n refs.${sql}.setField('\${3:TotalAmount}', Math.round(total * 100) / 100)\n}`, code: `if (['\${1:Quantity}', '\${2:UnitPrice}'].includes(event.field)) {\n const total = Number(event.record.\${1:Quantity} || 0) * Number(event.record.\${2:UnitPrice} || 0)\n refs.${sql}.setField('\${3:TotalAmount}', Math.round(total * 100) / 100)\n}`,
}, },
{ {
label: 'Kaydetmeden önce doğrula', label: 'App.VisualDesigner.Snippet.ValidateBeforeSave.Label',
group: 'Akış', group: 'App.VisualDesigner.Snippet.Group.Flow',
description: 'Eksik alan varsa işlemi iptal eder.', description: 'App.VisualDesigner.Snippet.ValidateBeforeSave.Description',
code: `const eksik = ['\${1:Name}', '\${2:Code}'].filter((column) => !event.record[column])\nif (eksik.length) {\n notify('Zorunlu alanlar: ' + eksik.join(', '), 'warning')\n return false\n}`, code: `const eksik = ['\${1:Name}', '\${2:Code}'].filter((column) => !event.record[column])\nif (eksik.length) {\n notify('Zorunlu alanlar: ' + eksik.join(', '), 'warning')\n return false\n}`,
}, },
{ {
label: 'Çok koşullu blok', label: 'App.VisualDesigner.Snippet.MultiCondition.Label',
group: 'Akış', group: 'App.VisualDesigner.Snippet.Group.Flow',
description: 'VE / VEYA ile birleşen koşullar.', description: 'App.VisualDesigner.Snippet.MultiCondition.Description',
code: `if (refs.${sql}.getMode() === 'new' || Number(refs.${sql}.getField('\${1:Total}') || 0) > \${2:1000}) {\n \${3:notify('Kontrol edin')}\n}`, code: `if (refs.${sql}.getMode() === 'new' || Number(refs.${sql}.getField('\${1:Total}') || 0) > \${2:1000}) {\n \${3:notify('Kontrol edin')}\n}`,
}, },
{ {
label: 'Toplu alan yazma', label: 'App.VisualDesigner.Snippet.BulkSet.Label',
group: 'Kayıt', group: 'App.VisualDesigner.Snippet.Group.Record',
description: 'Birden fazla sütunu tek çağrıda doldurur.', description: 'App.VisualDesigner.Snippet.BulkSet.Description',
code: `refs.${sql}.setFields({ \${1:Status}: '\${2:Taslak}', \${3:Currency}: '\${4:TRY}' })`, code: `refs.${sql}.setFields({ \${1:Status}: '\${2:Taslak}', \${3:Currency}: '\${4:TRY}' })`,
}, },
{ {
label: 'Komponent görünürlüğü', label: 'App.VisualDesigner.Snippet.Visibility.Label',
group: 'Komponent', group: 'App.VisualDesigner.Snippet.Group.Component',
description: 'Koşula göre gösterir veya gizler.', description: 'App.VisualDesigner.Snippet.Visibility.Description',
code: `refs.\${1:card1}.setVisible(refs.${sql}.getMode() === '\${2:edit}')`, code: `refs.\${1:card1}.setVisible(refs.${sql}.getMode() === '\${2:edit}')`,
}, },
{ {
label: 'API çağrısı ve hata yakalama', label: 'App.VisualDesigner.Snippet.ApiCall.Label',
group: 'Entegrasyon', group: 'App.VisualDesigner.Snippet.Group.Integration',
description: 'Çağrı başarısızsa bildirim gösterir.', description: 'App.VisualDesigner.Snippet.ApiCall.Description',
code: `try {\n const sonuc = await api.get('\${1:/api/app/customer/1}')\n refs.${sql}.setField('\${2:Title}', sonuc?.\${3:name})\n} catch (error) {\n notify(api.errorMessage(error), 'danger')\n}`, code: `try {\n const sonuc = await api.get('\${1:/api/app/customer/1}')\n refs.${sql}.setField('\${2:Title}', sonuc?.\${3:name})\n} catch (error) {\n notify(api.errorMessage(error), 'danger')\n}`,
}, },
{ {
label: 'Satırlar arasında dolaş', label: 'App.VisualDesigner.Snippet.IterateRows.Label',
group: 'Kayıt', group: 'App.VisualDesigner.Snippet.Group.Record',
description: 'Select sonucundaki tüm satırları gezer.', description: 'App.VisualDesigner.Snippet.IterateRows.Description',
code: `refs.${sql}.getRows().forEach((row, index) => {\n \${1:console.log(index, row)}\n})`, code: `refs.${sql}.getRows().forEach((row, index) => {\n \${1:console.log(index, row)}\n})`,
}, },
{ {
label: 'Değişiklik varsa kaydet', label: 'App.VisualDesigner.Snippet.SaveIfChanged.Label',
group: 'Kayıt', group: 'App.VisualDesigner.Snippet.Group.Record',
description: 'Boşuna istek atmaz.', description: 'App.VisualDesigner.Snippet.SaveIfChanged.Description',
code: `if (refs.${sql}.hasChanges()) await refs.${sql}.save()`, code: `if (refs.${sql}.hasChanges()) await refs.${sql}.save()`,
}, },
{ {
label: 'Event içeriğini incele', label: 'App.VisualDesigner.Snippet.InspectEvent.Label',
group: 'Akış', group: 'App.VisualDesigner.Snippet.Group.Flow',
description: 'Geliştirirken event nesnesinin ne taşıdığını görmek için.', description: 'App.VisualDesigner.Snippet.InspectEvent.Description',
code: `console.log('\${1:event}', JSON.parse(JSON.stringify(event ?? null)))`, code: `console.log('\${1:event}', JSON.parse(JSON.stringify(event ?? null)))`,
}, },
] ]
@ -143,11 +144,14 @@ export type DesignerScriptDialectContext = {
eventInfo?: SqlDataSourceEvent eventInfo?: SqlDataSourceEvent
/** Event'in sahibi komponentin adı; başlıkta gösterilir. */ /** Event'in sahibi komponentin adı; başlıkta gösterilir. */
componentLabel?: string componentLabel?: string
/** `useLocalization` hook'undan gelen çeviri fonksiyonu. */
translate: TranslateFn
} }
export const createDesignerScriptDialect = ( export const createDesignerScriptDialect = (
context: DesignerScriptDialectContext, context: DesignerScriptDialectContext,
): ScriptDialect => { ): ScriptDialect => {
const t = context.translate
const sqlContext: DesignerScriptContext = { const sqlContext: DesignerScriptContext = {
sqlRef: context.sqlRef, sqlRef: context.sqlRef,
cancelable: Boolean(context.eventInfo?.cancelable), cancelable: Boolean(context.eventInfo?.cancelable),
@ -166,12 +170,23 @@ export const createDesignerScriptDialect = (
const recipeViews: ScriptRecipeView[] = usableRecipes.map((recipe) => ({ const recipeViews: ScriptRecipeView[] = usableRecipes.map((recipe) => ({
id: recipe.id, id: recipe.id,
label: recipe.label, label: t('::' + recipe.label),
group: recipe.group, group: recipe.group,
summary: recipe.summary, summary: t('::' + recipe.summary),
example: recipe.example, example: recipe.example,
conditionIsArgument: recipe.conditionIsArgument, conditionIsArgument: recipe.conditionIsArgument,
params: recipe.params.map(toParam), params: recipe.params.map(toParam).map((param) => ({
...param,
label: t('::' + param.label),
help: param.help ? t('::' + param.help) : undefined,
placeholder: param.placeholder?.startsWith('App.')
? t('::' + param.placeholder)
: param.placeholder,
choices: param.choices?.map((choice) => ({
...choice,
label: choice.label?.startsWith('App.') ? t('::' + choice.label) : choice.label,
})),
})),
})) }))
const asDesignerRule = (rule: ScriptRule) => rule as unknown as DesignerScriptRule const asDesignerRule = (rule: ScriptRule) => rule as unknown as DesignerScriptRule
@ -181,25 +196,51 @@ export const createDesignerScriptDialect = (
title: 'Event Script', title: 'Event Script',
subject: `${context.componentLabel ? `${context.componentLabel}.` : ''}${context.eventName}`, subject: `${context.componentLabel ? `${context.componentLabel}.` : ''}${context.eventName}`,
subjectHint: context.eventInfo?.description, subjectHint: context.eventInfo?.description,
codeHint: context.eventInfo?.cancelable codeHint: t(
? 'event / refs / api / notify hazır. false döndürürsen işlem iptal edilir.' context.eventInfo?.cancelable
: 'event / refs / api / notify hazır. Ctrl+Space tamamlama listesini açar.', ? '::App.VisualDesigner.CodeHintCancelable'
: '::App.VisualDesigner.CodeHint',
),
groups: designerRecipeGroups.map((group) => ({ groups: designerRecipeGroups.map((group) => ({
key: group.key, key: group.key,
title: group.title, title: t('::' + group.title),
description: group.description, description: t('::' + group.description),
icon: groupIcons[group.key], icon: groupIcons[group.key],
})), })),
recipes: recipeViews, recipes: recipeViews,
operators: DEFAULT_SCRIPT_OPERATORS, operators: DEFAULT_SCRIPT_OPERATORS,
conditionKinds: [ conditionKinds: [
{ value: 'record', label: 'Kayıt sütunu', optionSource: RECORD_SOURCE }, {
{ value: 'ref', label: 'Komponent değeri', optionSource: REF_SOURCE }, value: 'record',
{ value: 'event', label: 'Event verisi', optionSource: EVENT_SOURCE }, label: t('::App.VisualDesigner.ConditionKind.RecordColumn'),
{ value: 'expression', label: 'Serbest ifade', placeholder: `refs.${context.sqlRef}.getMode()` }, optionSource: RECORD_SOURCE,
},
{
value: 'ref',
label: t('::App.VisualDesigner.ConditionKind.ComponentValue'),
optionSource: REF_SOURCE,
},
{
value: 'event',
label: t('::App.VisualDesigner.ConditionKind.EventData'),
optionSource: EVENT_SOURCE,
},
{
value: 'expression',
label: t('::App.VisualDesigner.ConditionKind.FreeExpression'),
placeholder: `refs.${context.sqlRef}.getMode()`,
},
], ],
apiReference: designerRuntimeApiReference, apiReference: designerRuntimeApiReference.map((entry) => ({
snippets: snippets(context.sqlRef), ...entry,
description: t('::' + entry.description),
})),
snippets: snippets(context.sqlRef).map((snippet) => ({
...snippet,
label: t('::' + snippet.label),
group: t('::' + snippet.group),
description: t('::' + snippet.description),
})),
getOptions: (optionSource) => getOptions: (optionSource) =>
optionSource === RECORD_SOURCE optionSource === RECORD_SOURCE
? recordFields ? recordFields
@ -219,27 +260,28 @@ export const createDesignerScriptDialect = (
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] } return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
}, },
describeCondition: (condition) => describeCondition: (condition) =>
describeDesignerCondition(condition as unknown as DesignerRuleCondition), describeDesignerCondition(condition as unknown as DesignerRuleCondition, t),
describeRule: (rule) => { describeRule: (rule) => {
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId) const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
if (!recipe) return '' if (!recipe) return ''
const action = recipe.describe(asDesignerRule(rule)) const action = recipe.describe(asDesignerRule(rule), t)
if (recipe.conditionIsArgument) return action if (recipe.conditionIsArgument) return action
const always = t('::App.VisualDesigner.Describe.Always')
const conditions = [rule.condition, ...(rule.conditions ?? [])] const conditions = [rule.condition, ...(rule.conditions ?? [])]
.map((condition) => .map((condition) =>
describeDesignerCondition(condition as unknown as DesignerRuleCondition), describeDesignerCondition(condition as unknown as DesignerRuleCondition, t),
) )
.filter((text) => text !== 'Her zaman') .filter((text) => text !== always)
const prefix = conditions.length const joiner =
? conditions.join(rule.join === 'or' ? ' veya ' : ' ve ') rule.join === 'or'
: 'Her zaman' ? ` ${t('::App.VisualDesigner.Word.Or')} `
: ` ${t('::App.VisualDesigner.Word.And')} `
const prefix = conditions.length ? conditions.join(joiner) : always
return `${prefix}${action}` return `${prefix}${action}`
}, },
getRuleWarning: (rule) => { getRuleWarning: (rule) => {
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId) const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
return recipe?.needsSqlRef && !context.sqlRef return recipe?.needsSqlRef && !context.sqlRef ? t('::App.VisualDesigner.SqlRefRequired') : ''
? 'Bu kural bir SqlDataSource ref adı gerektiriyor.'
: ''
}, },
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -444,25 +444,25 @@ export const SQL_DATA_SOURCE_SLOTS: readonly SqlDataSourceSlot[] = [
property: 'selectEndpoint', property: 'selectEndpoint',
method: 'GET', method: 'GET',
label: 'Select (GET)', label: 'Select (GET)',
description: 'Kaydı okuyan endpoint. İçerideki komponentler bu sütunlara bağlanır.', description: 'App.VisualDesigner.Event.X.Description',
}, },
{ {
property: 'insertEndpoint', property: 'insertEndpoint',
method: 'POST', method: 'POST',
label: 'Insert (POST)', label: 'Insert (POST)',
description: 'Key alanı boşken Save butonu bu endpointi çağırır.', description: 'App.VisualDesigner.Event.X.Description2',
}, },
{ {
property: 'updateEndpoint', property: 'updateEndpoint',
method: 'PUT', method: 'PUT',
label: 'Update (PUT)', label: 'Update (PUT)',
description: 'Key alanı doluyken Save butonu bu endpointi çağırır.', description: 'App.VisualDesigner.Event.X.Description3',
}, },
{ {
property: 'deleteEndpoint', property: 'deleteEndpoint',
method: 'DELETE', method: 'DELETE',
label: 'Delete (DELETE)', label: 'Delete (DELETE)',
description: 'Delete butonunu aktifleştirir.', description: 'App.VisualDesigner.Event.X.Description4',
}, },
] as const ] as const
@ -489,101 +489,101 @@ export interface SqlDataSourceEvent {
export const SQL_DATA_SOURCE_EVENTS: readonly SqlDataSourceEvent[] = [ export const SQL_DATA_SOURCE_EVENTS: readonly SqlDataSourceEvent[] = [
{ {
name: 'onLoad', name: 'onLoad',
label: 'Select tamamlandı', label: 'App.VisualDesigner.Event.OnLoad.Label',
description: 'Select endpointi cevap verdiğinde, satırlar forma yerleştikten sonra çalışır.', description: 'App.VisualDesigner.Event.OnLoad.Description',
payload: [ payload: [
{ key: 'rows', description: 'Select sonucundaki tüm satırlar' }, { key: 'rows', description: 'App.VisualDesigner.Event.Rows.Description' },
{ key: 'count', description: 'Satır sayısı' }, { key: 'count', description: 'App.VisualDesigner.Event.Count.Description' },
{ key: 'record', description: 'Aktif satır' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description' },
{ key: 'index', description: 'Aktif satırın sırası' }, { key: 'index', description: 'App.VisualDesigner.Event.Index.Description' },
], ],
}, },
{ {
name: 'onRecordChange', name: 'onRecordChange',
label: 'Aktif kayıt değişti', label: 'App.VisualDesigner.Event.OnRecordChange.Label',
description: 'Satırlar arasında gezinildiğinde veya yeni kayıt yüklendiğinde çalışır.', description: 'App.VisualDesigner.Event.OnRecordChange.Description',
payload: [ payload: [
{ key: 'record', description: 'Yeni aktif kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description2' },
{ key: 'index', description: 'Satır sırası' }, { key: 'index', description: 'App.VisualDesigner.Event.Index.Description2' },
{ key: 'rows', description: 'Tüm satırlar' }, { key: 'rows', description: 'App.VisualDesigner.Event.Rows.Description2' },
{ key: 'count', description: 'Satır sayısı' }, { key: 'count', description: 'App.VisualDesigner.Event.Count.Description' },
], ],
}, },
{ {
name: 'onFieldChange', name: 'onFieldChange',
label: 'Alan değişti', label: 'App.VisualDesigner.Event.OnFieldChange.Label',
description: 'İçerideki bir komponent kayda değer yazdığında çalışır; hesaplama için idealdir.', description: 'App.VisualDesigner.Event.OnFieldChange.Description',
payload: [ payload: [
{ key: 'field', description: 'Değişen sütun adı' }, { key: 'field', description: 'App.VisualDesigner.Event.Field.Description' },
{ key: 'value', description: 'Yeni değer' }, { key: 'value', description: 'App.VisualDesigner.Event.Value.Description' },
{ key: 'previous', description: 'Önceki değer' }, { key: 'previous', description: 'App.VisualDesigner.Event.Previous.Description' },
{ key: 'record', description: 'Değişiklik uygulanmış kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description3' },
], ],
}, },
{ {
name: 'onNewRecord', name: 'onNewRecord',
label: 'Yeni kayıt', label: 'App.VisualDesigner.Event.OnNewRecord.Label',
description: 'Yeni butonuyla boş kayda geçildiğinde çalışır; varsayılan değer yazmak için.', description: 'App.VisualDesigner.Event.OnNewRecord.Description',
payload: [{ key: 'record', description: 'Boş kayıt' }], payload: [{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description4' }],
}, },
{ {
name: 'onModeChange', name: 'onModeChange',
label: 'Mod değişti', label: 'App.VisualDesigner.Event.OnModeChange.Label',
description: 'Kayıt new/edit modları arasında geçtiğinde çalışır.', description: 'App.VisualDesigner.Event.OnModeChange.Description',
payload: [ payload: [
{ key: 'mode', description: '"new" veya "edit"' }, { key: 'mode', description: '"new" veya "edit"' },
{ key: 'record', description: 'Güncel kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description5' },
], ],
}, },
{ {
name: 'onBeforeSave', name: 'onBeforeSave',
label: 'Kaydetmeden önce', label: 'App.VisualDesigner.Event.OnBeforeSave.Label',
description: 'Save çağrılmadan önce çalışır. Script false döndürürse kayıt iptal edilir.', description: 'App.VisualDesigner.Event.OnBeforeSave.Description',
cancelable: true, cancelable: true,
payload: [ payload: [
{ key: 'record', description: 'Kaydedilecek kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description6' },
{ key: 'original', description: 'Yüklendiği andaki hali' }, { key: 'original', description: 'App.VisualDesigner.Event.Original.Description' },
{ key: 'payload', description: 'Endpointe gidecek gövde (Update ise sadece değişenler)' }, { key: 'payload', description: 'App.VisualDesigner.Event.Payload.Description' },
{ key: 'mode', description: '"new" veya "edit"' }, { key: 'mode', description: '"new" veya "edit"' },
{ key: 'isNew', description: 'Insert mi çalışacak?' }, { key: 'isNew', description: 'App.VisualDesigner.Event.IsNew.Description' },
], ],
}, },
{ {
name: 'onAfterSave', name: 'onAfterSave',
label: 'Kaydettikten sonra', label: 'App.VisualDesigner.Event.OnAfterSave.Label',
description: 'Insert/Update başarıyla tamamlandığında çalışır.', description: 'App.VisualDesigner.Event.OnAfterSave.Description',
payload: [ payload: [
{ key: 'record', description: 'Gönderilen kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description7' },
{ key: 'response', description: 'Endpoint cevabı' }, { key: 'response', description: 'App.VisualDesigner.Event.Response.Description' },
{ key: 'mode', description: 'Kaydetme anındaki mod' }, { key: 'mode', description: 'App.VisualDesigner.Event.Mode.Description' },
{ key: 'isNew', description: 'Insert mi çalıştı?' }, { key: 'isNew', description: 'App.VisualDesigner.Event.IsNew.Description2' },
], ],
}, },
{ {
name: 'onBeforeDelete', name: 'onBeforeDelete',
label: 'Silmeden önce', label: 'App.VisualDesigner.Event.OnBeforeDelete.Label',
description: 'Onay kutusundan önce çalışır. Script false döndürürse silme iptal edilir.', description: 'App.VisualDesigner.Event.OnBeforeDelete.Description',
cancelable: true, cancelable: true,
payload: [ payload: [
{ key: 'record', description: 'Silinecek kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description8' },
{ key: 'key', description: 'Key sütunundaki değer' }, { key: 'key', description: 'App.VisualDesigner.Event.Key.Description' },
], ],
}, },
{ {
name: 'onAfterDelete', name: 'onAfterDelete',
label: 'Sildikten sonra', label: 'App.VisualDesigner.Event.OnAfterDelete.Label',
description: 'Delete endpointi başarıyla tamamlandığında çalışır.', description: 'App.VisualDesigner.Event.OnAfterDelete.Description',
payload: [{ key: 'record', description: 'Silinen kayıt' }], payload: [{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description9' }],
}, },
{ {
name: 'onError', name: 'onError',
label: 'Hata oluştu', label: 'App.VisualDesigner.Event.OnError.Label',
description: 'Select/Insert/Update/Delete sırasında hata alındığında çalışır.', description: 'App.VisualDesigner.Event.OnError.Description',
payload: [ payload: [
{ key: 'message', description: 'Kullanıcıya gösterilen hata metni' }, { key: 'message', description: 'App.VisualDesigner.Event.Message.Description' },
{ key: 'action', description: '"reload" | "save" | "delete"' }, { key: 'action', description: '"reload" | "save" | "delete"' },
{ key: 'error', description: 'Ham hata nesnesi' }, { key: 'error', description: 'App.VisualDesigner.Event.Error.Description' },
{ key: 'record', description: 'Hata anındaki kayıt' }, { key: 'record', description: 'App.VisualDesigner.Event.Record.Description10' },
], ],
}, },
] as const ] as const
@ -613,7 +613,7 @@ export const getSqlDataSourceKeyField = (node: DesignerNode) =>
*/ */
export const SQL_DATA_SOURCE_KEY_SOURCES = [ export const SQL_DATA_SOURCE_KEY_SOURCES = [
{ value: 'query', label: 'URL query string (?id=…)' }, { value: 'query', label: 'URL query string (?id=…)' },
{ value: 'route', label: 'URL son segmenti (/…/5)' }, { value: 'route', label: 'App.VisualDesigner.Event.OnError.Label2' },
] as const ] as const
export type SqlDataSourceKeySource = (typeof SQL_DATA_SOURCE_KEY_SOURCES)[number]['value'] export type SqlDataSourceKeySource = (typeof SQL_DATA_SOURCE_KEY_SOURCES)[number]['value']

View file

@ -67,13 +67,13 @@ export class OrderService {
} catch (error) { } catch (error) {
console.error('Order creation error:', error) console.error('Order creation error:', error)
let errorMessage = 'Sipariş oluşturulurken hata oluştu' let errorMessage = 'Public.payment.createOrderError'
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
if (error.response) { if (error.response) {
errorMessage = error.response.data?.message || errorMessage errorMessage = error.response.data?.message || errorMessage
} else if (error.request) { } else if (error.request) {
errorMessage = 'Sunucuya bağlanılamadı' errorMessage = 'App.Platform.ServerUnreachable'
} }
} }

View file

@ -5,7 +5,11 @@ import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification' import Notification from '@/components/ui/Notification'
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models' import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
export class SignalRService { export class SignalRService {
/** `useLocalization` hook'undan gelen çeviri fonksiyonu; bildirimlerde kullanılır. */
private translate: TranslateFn = (key) => key
private connection!: signalR.HubConnection private connection!: signalR.HubConnection
private isConnected: boolean = false private isConnected: boolean = false
private currentSessionId?: string private currentSessionId?: string
@ -32,6 +36,10 @@ export class SignalRService {
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
private onForceCleanup?: () => void private onForceCleanup?: () => void
setTranslate(translate: TranslateFn) {
this.translate = translate
}
constructor() { constructor() {
const { auth } = store.getState() const { auth } = store.getState()
@ -99,9 +107,15 @@ export class SignalRService {
this.connection.onreconnected(async () => { this.connection.onreconnected(async () => {
this.isConnected = true this.isConnected = true
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.Reconnected')}
type="success"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
if (this.currentSessionId && store.getState().auth.user) { if (this.currentSessionId && store.getState().auth.user) {
const u = store.getState().auth.user const u = store.getState().auth.user
@ -112,7 +126,10 @@ export class SignalRService {
this.connection.onclose(async () => { this.connection.onclose(async () => {
if (this.isKicked) { if (this.isKicked) {
toast.push( toast.push(
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />, <Notification
title={this.translate('::App.Videoroom.Signal.Reconnecting')}
type="warning"
/>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
this.isConnected = false this.isConnected = false
@ -131,15 +148,27 @@ export class SignalRService {
}) })
this.connection.on('Error', (message: string) => { this.connection.on('Error', (message: string) => {
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.Error', { message })}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
}) })
this.connection.on('Warning', (message: string) => { this.connection.on('Warning', (message: string) => {
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.Warning', { message })}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
}) })
this.connection.on('Info', (message: string) => { this.connection.on('Info', (message: string) => {
@ -152,7 +181,7 @@ export class SignalRService {
if (this.isKicked) { if (this.isKicked) {
toast.push( toast.push(
<Notification <Notification
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi" title={this.translate('::App.Videoroom.Signal.KickedNoReconnect')}
type="danger" type="danger"
/>, />,
) )
@ -163,9 +192,15 @@ export class SignalRService {
this.connection.on('ForceDisconnect', async (message: string) => { this.connection.on('ForceDisconnect', async (message: string) => {
this.isKicked = true this.isKicked = true
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.Kicked', { message })}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
if (this.onForceCleanup) { if (this.onForceCleanup) {
this.onForceCleanup() this.onForceCleanup()
@ -194,18 +229,24 @@ export class SignalRService {
try { try {
const startPromise = this.connection.start() const startPromise = this.connection.start()
const timeout = new Promise((_, reject) => const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000), setTimeout(
() => reject(new Error(this.translate('::App.Videoroom.Signal.Timeout'))),
10000,
),
) )
await Promise.race([startPromise, timeout]) await Promise.race([startPromise, timeout])
this.isConnected = true this.isConnected = true
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.Signal.Connected')} type="success" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} catch { } catch {
toast.push( toast.push(
<Notification <Notification
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin." title={this.translate('::App.Videoroom.Signal.ServerUnreachable')}
type="danger" type="danger"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -224,7 +265,7 @@ export class SignalRService {
if (!this.isConnected) { if (!this.isConnected) {
toast.push( toast.push(
<Notification <Notification
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin." title={this.translate('::App.Videoroom.Signal.NoConnection')}
type="warning" type="warning"
/>, />,
{ {
@ -238,9 +279,12 @@ export class SignalRService {
try { try {
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive) await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
} catch { } catch {
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.Signal.JoinFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -256,9 +300,15 @@ export class SignalRService {
await this.connection.invoke('LeaveClass', sessionId) await this.connection.invoke('LeaveClass', sessionId)
this.currentSessionId = undefined this.currentSessionId = undefined
} catch { } catch {
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.LeaveFailed')}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -297,9 +347,15 @@ export class SignalRService {
'public', 'public',
) )
} catch { } catch {
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.PublicMessageFailed')}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -344,9 +400,15 @@ export class SignalRService {
'private', 'private',
) )
} catch { } catch {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.PrivateMessageFailed')}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -384,9 +446,12 @@ export class SignalRService {
isTeacher, isTeacher,
) )
} catch { } catch {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.AnnouncementFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -406,9 +471,12 @@ export class SignalRService {
try { try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher) await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch { } catch {
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.MuteFailed')} type="warning" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -423,9 +491,15 @@ export class SignalRService {
try { try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName) await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch { } catch {
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.RaiseHandFailed')}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -440,9 +514,12 @@ export class SignalRService {
try { try {
await this.connection.invoke('KickParticipant', sessionId, participantId) await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch { } catch {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.KickFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -457,9 +534,15 @@ export class SignalRService {
try { try {
await this.connection.invoke('ApproveHandRaise', sessionId, studentId) await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
} catch { } catch {
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.HandRaiseApproveFailed')}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -474,9 +557,15 @@ export class SignalRService {
try { try {
await this.connection.invoke('DismissHandRaise', sessionId, studentId) await this.connection.invoke('DismissHandRaise', sessionId, studentId)
} catch { } catch {
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.LowerHandFailed')}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
@ -554,9 +643,15 @@ export class SignalRService {
try { try {
await this.connection.invoke('LeaveClass', this.currentSessionId) await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch { } catch {
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Signal.DisconnectError')}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
if (this.connection) { if (this.connection) {

View file

@ -1,13 +1,21 @@
import { toast } from '@/components/ui' import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification' import Notification from '@/components/ui/Notification'
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
export class WebRTCService { export class WebRTCService {
/** `useLocalization` hook'undan gelen çeviri fonksiyonu; bildirimlerde kullanılır. */
private translate: TranslateFn = (key) => key
private peerConnections: Map<string, RTCPeerConnection> = new Map() private peerConnections: Map<string, RTCPeerConnection> = new Map()
private retryCounts: Map<string, number> = new Map() private retryCounts: Map<string, number> = new Map()
private maxRetries = 3 private maxRetries = 3
private signalRService: any private signalRService: any
private sessionId: string = '' private sessionId: string = ''
setTranslate(translate: TranslateFn) {
this.translate = translate
}
private localStream: MediaStream | null = null private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void private onRemoteStream?: (userId: string, stream: MediaStream) => void
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
@ -50,7 +58,7 @@ export class WebRTCService {
} catch { } catch {
toast.push( toast.push(
<Notification <Notification
title="❌ Kamera/Mikrofon erişilemedi. Tarayıcı ayarlarınızı veya izinleri kontrol edin." title={this.translate('::App.Videoroom.Rtc.MediaAccessFailed')}
type="danger" type="danger"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -93,7 +101,10 @@ export class WebRTCService {
if (retries < this.maxRetries) { if (retries < this.maxRetries) {
toast.push( toast.push(
<Notification <Notification
title={`⚠️ Bağlantı başarısız, yeniden deneniyor (${retries + 1}/${this.maxRetries})`} title={this.translate('::App.Videoroom.Rtc.RetryingConnection', {
current: retries + 1,
total: this.maxRetries,
})}
type="warning" type="warning"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -103,7 +114,9 @@ export class WebRTCService {
} else { } else {
toast.push( toast.push(
<Notification <Notification
title={`❌ Bağlantı kurulamadı (${this.maxRetries} deneme başarısız).`} title={this.translate('::App.Videoroom.Rtc.ConnectionFailed', {
total: this.maxRetries,
})}
type="danger" type="danger"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -120,7 +133,7 @@ export class WebRTCService {
} catch { } catch {
toast.push( toast.push(
<Notification <Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`} title={this.translate('::App.Videoroom.Rtc.IceCandidateFailed', { userId })}
type="warning" type="warning"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -151,9 +164,12 @@ export class WebRTCService {
await pc.setLocalDescription(offer) await pc.setLocalDescription(offer)
return offer return offer
} catch { } catch {
toast.push(<Notification title="❌ Offer oluşturulamadı" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.Rtc.OfferFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
throw new Error('Offer creation failed') throw new Error('Offer creation failed')
} }
} }
@ -171,9 +187,12 @@ export class WebRTCService {
await pc.setLocalDescription(answer) await pc.setLocalDescription(answer)
return answer return answer
} catch { } catch {
toast.push(<Notification title="❌ Answer oluşturulamadı" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.Rtc.AnswerFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
throw new Error('Answer creation failed') throw new Error('Answer creation failed')
} }
} }
@ -200,7 +219,7 @@ export class WebRTCService {
} catch { } catch {
toast.push( toast.push(
<Notification <Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`} title={this.translate('::App.Videoroom.Rtc.IceCandidateFailed', { userId })}
type="warning" type="warning"
/>, />,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -240,9 +259,12 @@ export class WebRTCService {
}) })
} }
} catch { } catch {
toast.push(<Notification title="❌ Kamera açılamadı" type="danger" />, { toast.push(
<Notification title={this.translate('::App.Videoroom.Rtc.CameraFailed')} type="danger" />,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
} }
@ -269,9 +291,15 @@ export class WebRTCService {
}) })
} }
} catch { } catch {
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Rtc.MicrophoneFailed')}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }
} }
@ -288,14 +316,26 @@ export class WebRTCService {
if (this.signalRService) { if (this.signalRService) {
await this.signalRService.sendOffer(this.sessionId, userId, offer) await this.signalRService.sendOffer(this.sessionId, userId, offer)
} else { } else {
toast.push(<Notification title="⚠️ Tekrar bağlanma başarısız" type="warning" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Rtc.ReconnectFailed')}
type="warning"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} catch { } catch {
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, { toast.push(
<Notification
title={this.translate('::App.Videoroom.Rtc.IceRestartFailed')}
type="danger"
/>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
} }
} }

View file

@ -17,7 +17,7 @@ export const useOrders = () => {
setError(null) setError(null)
} catch (err) { } catch (err) {
console.error('Products fetch error:', err) console.error('Products fetch error:', err)
setError('Ürünler yüklenirken hata oluştu') setError('App.Orders.LoadProductsError')
} finally { } finally {
setLoading(false) setLoading(false)
} }

View file

@ -1,24 +1,26 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
export function usePWA(): boolean { export function usePWA(): boolean {
const [isPWA, setIsPWA] = useState(false); const [isPWA, setIsPWA] = useState(false)
useEffect(() => { useEffect(() => {
const checkPWA = () => { const checkPWA = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone; const isStandalone =
setIsPWA(isStandalone); window.matchMedia('(display-mode: standalone)').matches ||
}; (window.navigator as any).standalone
setIsPWA(isStandalone)
}
checkPWA(); // İlk kontrolü yap checkPWA() // İlk kontrolü yap
// display-mode değiştiğinde güncellenmesi için event listener ekle // display-mode değiştiğinde güncellenmesi için event listener ekle
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA); window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA)
return () => { return () => {
// Event listener'ı temizle // Event listener'ı temizle
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA); window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA)
}; }
}, []); }, [])
return isPWA; return isPWA
} }

View file

@ -1,19 +1,17 @@
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from "react-icons/fi"; import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from 'react-icons/fi'
export const kindOptions = [ export const kindOptions = [
{ value: "Start", label: "Başlat" }, { value: 'Start', label: 'ListForms.Workflow.Kind.Start' },
{ value: "Compare", label: "Karşılaştırma" }, { value: 'Compare', label: 'ListForms.Workflow.Kind.Compare' },
{ value: "Approval", label: "Onaylanacak kişi" }, { value: 'Approval', label: 'ListForms.Workflow.Kind.Approval' },
{ value: "Inform", label: "Bilgilendirme" }, { value: 'Inform', label: 'ListForms.Workflow.Kind.Inform' },
{ value: "End", label: "Akışı bitir" }, { value: 'End', label: 'ListForms.Workflow.Kind.End' },
]; ]
export const operatorOptions = [">", ">=", "<", "<=", "=", "!="].map( export const operatorOptions = ['>', '>=', '<', '<=', '=', '!='].map((value) => ({
(value) => ({
value, value,
label: value, label: value,
}), }))
);
export const kindIcon: Record<string, any> = { export const kindIcon: Record<string, any> = {
Start: FiPlay as any, Start: FiPlay as any,
@ -21,13 +19,13 @@ export const kindIcon: Record<string, any> = {
Approval: FiCheck as any, Approval: FiCheck as any,
Inform: FiBell as any, Inform: FiBell as any,
End: FiSlash as any, End: FiSlash as any,
}; }
export const nodeSize = { export const nodeSize = {
width: 176, width: 176,
height: 128, height: 128,
}; }
export function getNodeHeight(item: { kind: string }) { export function getNodeHeight(item: { kind: string }) {
return item?.kind === "Compare" ? 158 : nodeSize.height; return item?.kind === 'Compare' ? 158 : nodeSize.height
} }

View file

@ -153,27 +153,27 @@ export function collectLinks(criteria: WorkflowCriteriaDto[]) {
return return
} }
addLink(links, criteria, source, source.nextOnStart, 'Sonraki', 'next', { addLink(links, criteria, source, source.nextOnStart, OUTCOME_KEYS.nextOnStart, 'next', {
index: 0, index: 0,
count: 1, count: 1,
field: 'nextOnStart', field: 'nextOnStart',
}) })
addLink(links, criteria, source, source.nextOnTrue, 'Doğru', 'true', { addLink(links, criteria, source, source.nextOnTrue, OUTCOME_KEYS.nextOnTrue, 'true', {
index: 0, index: 0,
count: 2, count: 2,
field: 'nextOnTrue', field: 'nextOnTrue',
}) })
addLink(links, criteria, source, source.nextOnFalse, 'Yanlış', 'false', { addLink(links, criteria, source, source.nextOnFalse, OUTCOME_KEYS.nextOnFalse, 'false', {
index: 1, index: 1,
count: 2, count: 2,
field: 'nextOnFalse', field: 'nextOnFalse',
}) })
addLink(links, criteria, source, source.nextOnApprove, 'Onay', 'approve', { addLink(links, criteria, source, source.nextOnApprove, OUTCOME_KEYS.nextOnApprove, 'approve', {
index: 0, index: 0,
count: 2, count: 2,
field: 'nextOnApprove', field: 'nextOnApprove',
}) })
addLink(links, criteria, source, source.nextOnReject, 'Red', 'reject', { addLink(links, criteria, source, source.nextOnReject, OUTCOME_KEYS.nextOnReject, 'reject', {
index: 1, index: 1,
count: 2, count: 2,
field: 'nextOnReject', field: 'nextOnReject',
@ -242,43 +242,53 @@ function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
return dy >= 0 ? 'bottom' : 'top' return dy >= 0 ? 'bottom' : 'top'
} }
/** Çıkış etiketleri lokalizasyon anahtarı olarak taşınır; ekranda `translate` ile çözülür. */
export const OUTCOME_KEYS = {
nextOnStart: 'ListForms.Workflow.Outcome.Next',
nextOnTrue: 'ListForms.Workflow.Outcome.True',
nextOnFalse: 'ListForms.Workflow.Outcome.False',
nextOnApprove: 'ListForms.Workflow.Outcome.Approve',
nextOnReject: 'ListForms.Workflow.Outcome.Reject',
compare: 'ListForms.Workflow.Outcome.CompareState',
} as const
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] { export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
if (item.kind === 'Compare') { if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes?.length const outcomes = item.compareOutcomes?.length
? item.compareOutcomes ? item.compareOutcomes
: [ : [
{ label: 'Doğru', targetId: item.nextOnTrue }, { label: OUTCOME_KEYS.nextOnTrue, targetId: item.nextOnTrue },
{ label: 'Yanlış', targetId: item.nextOnFalse }, { label: OUTCOME_KEYS.nextOnFalse, targetId: item.nextOnFalse },
] ]
return outcomes.slice(0, 4).map((outcome, index) => ({ return outcomes.slice(0, 4).map((outcome, index) => ({
field: `compareOutcomes:${index}`, field: `compareOutcomes:${index}`,
label: outcome.label || `Durum ${index + 1}`, label: outcome.label || `#${index + 1}`,
targetId: outcome.targetId, targetId: outcome.targetId,
})) }))
} }
if (item.kind === 'Approval') { if (item.kind === 'Approval') {
return [ return [
{ field: 'nextOnApprove', label: 'Onay', targetId: item.nextOnApprove }, { field: 'nextOnApprove', label: OUTCOME_KEYS.nextOnApprove, targetId: item.nextOnApprove },
{ field: 'nextOnReject', label: 'Red', targetId: item.nextOnReject }, { field: 'nextOnReject', label: OUTCOME_KEYS.nextOnReject, targetId: item.nextOnReject },
] ]
} }
if (item.kind === 'End') return [] if (item.kind === 'End') return []
return [{ field: 'nextOnStart', label: 'Sonraki', targetId: item.nextOnStart }] return [{ field: 'nextOnStart', label: OUTCOME_KEYS.nextOnStart, targetId: item.nextOnStart }]
} }
export function outcomeLabel(field?: string) { export function outcomeLabel(field?: string) {
if (field?.startsWith('compareOutcomes:')) return 'Karşılaştırma durumu' if (field?.startsWith('compareOutcomes:')) return OUTCOME_KEYS.compare
const labels: Record<string, string> = { const labels: Record<string, string> = {
nextOnStart: 'Sonraki', nextOnStart: OUTCOME_KEYS.nextOnStart,
nextOnTrue: 'Doğru', nextOnTrue: OUTCOME_KEYS.nextOnTrue,
nextOnFalse: 'Yanlış', nextOnFalse: OUTCOME_KEYS.nextOnFalse,
nextOnApprove: 'Onay', nextOnApprove: OUTCOME_KEYS.nextOnApprove,
nextOnReject: 'Red', nextOnReject: OUTCOME_KEYS.nextOnReject,
} }
return field ? labels[field] : undefined return field ? labels[field] : undefined
@ -406,7 +416,9 @@ function formatCriteriaId(number: number) {
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) { function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
const normalized = (title || '').trim() const normalized = (title || '').trim()
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized) return (
normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
)
} }
function escapeRegExp(value: string) { function escapeRegExp(value: string) {
@ -432,8 +444,7 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
} }
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput { export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson = const sharedPerson = item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
const compareOutcomes = (item.compareOutcomes || []) const compareOutcomes = (item.compareOutcomes || [])
.slice(0, 4) .slice(0, 4)
.filter((outcome) => outcome.label?.trim()) .filter((outcome) => outcome.label?.trim())
@ -465,12 +476,12 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
export function defaultTitle(kind: string) { export function defaultTitle(kind: string) {
return ( return (
{ {
Start: 'İş Akışı Başlat', Start: 'ListForms.Workflow.Kind.Start',
Compare: 'Karşılaştırma', Compare: 'ListForms.Workflow.Kind.Compare',
Approval: 'Onay', Approval: 'ListForms.Workflow.Kind.Approval',
Inform: 'Bilgilendirme', Inform: 'ListForms.Workflow.Kind.Inform',
End: 'İş Akışı Bitir', End: 'ListForms.Workflow.Kind.End',
}[kind] ?? 'İş Akışı Adımı' }[kind] ?? 'ListForms.Workflow.Kind.Step'
) )
} }
@ -570,7 +581,7 @@ export function compareOutcomeRuleText(
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`, `${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
) )
.join(' ve ') .join(' ve ')
: 'Kural yok' : 'ListForms.Workflow.NoRule'
} }
export function formatCompactValue(value: number | string | null | undefined) { export function formatCompactValue(value: number | string | null | undefined) {

View file

@ -14,6 +14,11 @@ import { useStoreState } from '@/store'
* Kullanıcı bilinçli olarak kurulum sihirbazına yönlendirilmez çalışan bir sistemde * Kullanıcı bilinçli olarak kurulum sihirbazına yönlendirilmez çalışan bir sistemde
* geçici bir arıza kimseyi kurulum ekranına düşürmemeli. Bunun yerine durum ıkça * geçici bir arıza kimseyi kurulum ekranına düşürmemeli. Bunun yerine durum ıkça
* belirtilir, sunucu arka planda izlenir ve toparlandığı anda sayfa kendini yeniler. * belirtilir, sunucu arka planda izlenir ve toparlandığı anda sayfa kendini yeniler.
*
* Metinler bilinçli olarak `translate` ile lokalize EDİLMEZ: bu ekran yalnızca
* konfigürasyon (ve onunla gelen lokalizasyon sözlüğü) yüklenemediğinde render edilir,
* dolayısıyla `translate` anahtarın kendisini döndürürdü. ılış öncesi tüm ekranlar
* bu nedenle sabit İngilizce tutulur.
*/ */
const ServiceUnavailable = () => { const ServiceUnavailable = () => {
const isDark = useStoreState((state) => state.theme.mode) === 'dark' const isDark = useStoreState((state) => state.theme.mode) === 'dark'

View file

@ -7,6 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary'
import type { FallbackProps } from 'react-error-boundary' import type { FallbackProps } from 'react-error-boundary'
import { FaArrowLeft } from 'react-icons/fa' import { FaArrowLeft } from 'react-icons/fa'
import { Navigate, useLocation } from 'react-router-dom' import { Navigate, useLocation } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DialogProvider from './shared/DialogContext' import DialogProvider from './shared/DialogContext'
import DialogShowComponent from './shared/DialogContext/DialogShowComponent' import DialogShowComponent from './shared/DialogContext/DialogShowComponent'
import UiDialog from './shared/UiDialog' import UiDialog from './shared/UiDialog'
@ -20,13 +21,16 @@ interface ViewsProps {
// react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor; // react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor;
// throw edilen her şey Error olmayabilir, o yüzden daraltıyoruz. // throw edilen her şey Error olmayabilir, o yüzden daraltıyoruz.
function fallbackRender({ error, resetErrorBoundary }: FallbackProps) { function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
const { translate } = useLocalization()
const { name, message } = const { name, message } =
error instanceof Error ? error : { name: 'Hata!', message: String(error) } error instanceof Error
? error
: { name: translate('::App.Platform.Error'), message: String(error) }
return ( return (
<Alert showIcon className="mb-4" type="danger"> <Alert showIcon className="mb-4" type="danger">
<h5>{name ?? 'Hata!'}</h5> <h5>{name ?? translate('::App.Platform.Error')}</h5>
<div>{message}</div> <div>{message}</div>
<Button <Button
icon={<FaArrowLeft />} icon={<FaArrowLeft />}
@ -49,7 +53,7 @@ const Views = (props: ViewsProps) => {
}, [location, setWarning]) }, [location, setWarning])
return ( return (
<ErrorBoundary fallbackRender={fallbackRender}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loading loading={true} />}> <Suspense fallback={<Loading loading={true} />}>
{!!warning?.length && ( {!!warning?.length && (
<Alert showIcon className="mb-4 text-sm text-left" type="warning"> <Alert showIcon className="mb-4 text-sm text-left" type="warning">

View file

@ -4,6 +4,7 @@ import { NotificationChannelColors } from '@/constants/notification-channel.enum
import { NotificationDto } from '@/proxy/notification/models' import { NotificationDto } from '@/proxy/notification/models'
import classNames from 'classnames' import classNames from 'classnames'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useLocalization } from '@/utils/hooks/useLocalization'
type EventProps = { type EventProps = {
data: NotificationDto data: NotificationDto
@ -19,6 +20,8 @@ const HighlightedText = ({ children, className }: CommonProps) => {
} }
const Event = ({ data, compact }: EventProps) => { const Event = ({ data, compact }: EventProps) => {
const { translate } = useLocalization()
return ( return (
<> <>
{compact ? ( {compact ? (
@ -28,7 +31,11 @@ const Event = ({ data, compact }: EventProps) => {
<span className="text-xs">{dayjs(data.creationTime).format('hh:mm A')}</span> <span className="text-xs">{dayjs(data.creationTime).format('hh:mm A')}</span>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<span className="mx-1">bildirim gönderdi {data.notificationType} </span> <span className="mx-1">
{translate('::Abp.Identity.ActivityLogs.SentNotificationOfType', {
type: data.notificationType,
})}{' '}
</span>
<Tag <Tag
prefix prefix
@ -42,7 +49,8 @@ const Event = ({ data, compact }: EventProps) => {
) : ( ) : (
<div className="my-1 flex items-center"> <div className="my-1 flex items-center">
<div> <div>
<HighlightedText>{data.notificationType}</HighlightedText> bildirimi gönderdi. <HighlightedText>{data.notificationType}</HighlightedText>{' '}
{translate('::Abp.Identity.ActivityLogs.SentNotification')}
</div> </div>
<Tag <Tag

View file

@ -35,7 +35,9 @@ const Log = ({
</div> </div>
<Timeline className="dark:bg-gray-800"> <Timeline className="dark:bg-gray-800">
{isEmpty(notifications[group]) ? ( {isEmpty(notifications[group]) ? (
<Timeline.Item className="dark:text-gray-400">Bildirim yok</Timeline.Item> <Timeline.Item className="dark:text-gray-400">
{translate('::Abp.Identity.ActivityLogs.NoNotification')}
</Timeline.Item>
) : ( ) : (
notifications[group].map((notification) => ( notifications[group].map((notification) => (
<Timeline.Item <Timeline.Item
@ -56,11 +58,13 @@ const Log = ({
))} ))}
<div className="text-center"> <div className="text-center">
{loadable ? ( {loadable ? (
<Button size="sm" loading={isLoading} variant='solid' onClick={onLoadMore}> <Button size="sm" loading={isLoading} variant="solid" onClick={onLoadMore}>
{translate('::Abp.Identity.ActivityLogs.LoadMore')} {translate('::Abp.Identity.ActivityLogs.LoadMore')}
</Button> </Button>
) : ( ) : (
<span className="dark:text-gray-400">{translate('::Abp.Identity.ActivityLogs.ReceivedAllNotifications')}</span> <span className="dark:text-gray-400">
{translate('::Abp.Identity.ActivityLogs.ReceivedAllNotifications')}
</span>
)} )}
</div> </div>
</div> </div>

View file

@ -44,8 +44,15 @@ const LogFilter = ({
const { translate } = useLocalization() const { translate } = useLocalization()
const content = ( const content = (
<div className={classNames('rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4', className)}> <div
<h5 className="mb-4 text-base font-semibold text-gray-900 dark:text-white">{translate('::Abp.Identity.ActivityLogs.Filters')}</h5> className={classNames(
'rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4',
className,
)}
>
<h5 className="mb-4 text-base font-semibold text-gray-900 dark:text-white">
{translate('::Abp.Identity.ActivityLogs.Filters')}
</h5>
<Checkbox.Group <Checkbox.Group
vertical vertical
value={filter} value={filter}

View file

@ -130,7 +130,15 @@ const getFileTypeLabel = (item: FileItemType, translate: (key: string) => string
// Defined at module level: nesting it inside FileItem would remount the <img> // Defined at module level: nesting it inside FileItem would remount the <img>
// (and restart the download) on every parent render. // (and restart the download) on every parent render.
const ImagePreview = ({ src, alt, fallback }: { src: string; alt: string; fallback: ReactNode }) => { const ImagePreview = ({
src,
alt,
fallback,
}: {
src: string
alt: string
fallback: ReactNode
}) => {
const [imageError, setImageError] = useState(false) const [imageError, setImageError] = useState(false)
return ( return (
@ -250,16 +258,16 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
navigator.clipboard.writeText(fileUrl).then( navigator.clipboard.writeText(fileUrl).then(
() => { () => {
toast.push( toast.push(
<Notification type="success" title="Başarılı"> <Notification type="success" title={translate('::App.Platform.Success')}>
Dosya URL&apos;si panoya kopyalandı {translate('::FileManager.UrlCopied')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
}, },
() => { () => {
toast.push( toast.push(
<Notification type="danger" title="Hata"> <Notification type="danger" title={translate('::App.Platform.Error')}>
URL kopyalanamadı {translate('::FileManager.UrlCopyFailed')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -438,7 +446,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</span> </span>
) : item.type === 'folder' && typeof item.childCount === 'number' ? ( ) : item.type === 'folder' && typeof item.childCount === 'number' ? (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{item.childCount} öğe {translate('::FileManager.ItemCount', { count: item.childCount })}
</span> </span>
) : ( ) : (
<span className="text-sm text-gray-500 dark:text-gray-400">-</span> <span className="text-sm text-gray-500 dark:text-gray-400">-</span>

View file

@ -47,7 +47,11 @@ export const CreateFolderModal = forwardRef<HTMLDivElement, CreateFolderModalPro
<Dialog isOpen={isOpen} onClose={handleClose}> <Dialog isOpen={isOpen} onClose={handleClose}>
<div ref={ref}> <div ref={ref}>
<form className="py-6" onSubmit={handleSubmit}> <form className="py-6" onSubmit={handleSubmit}>
<FormItem label={translate('::FileManager.FolderName')} invalid={!!error} errorMessage={error}> <FormItem
label={translate('::FileManager.FolderName')}
invalid={!!error}
errorMessage={error}
>
<Input <Input
autoFocus autoFocus
value={folderName} value={folderName}
@ -58,11 +62,11 @@ export const CreateFolderModal = forwardRef<HTMLDivElement, CreateFolderModalPro
</form> </form>
<div className="flex justify-end space-x-2 pt-4 border-t"> <div className="flex justify-end space-x-2 pt-4 border-t">
<Button size='sm' variant="default" disabled={loading} onClick={handleClose}> <Button size="sm" variant="default" disabled={loading} onClick={handleClose}>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button <Button
size='sm' size="sm"
variant="solid" variant="solid"
loading={loading} loading={loading}
disabled={!folderName.trim()} disabled={!folderName.trim()}
@ -156,11 +160,11 @@ export const RenameItemModal = forwardRef<HTMLDivElement, RenameItemModalProps>(
</form> </form>
<div className="flex justify-end space-x-2 pt-4 border-t"> <div className="flex justify-end space-x-2 pt-4 border-t">
<Button size='sm' variant="default" disabled={loading} onClick={handleClose}> <Button size="sm" variant="default" disabled={loading} onClick={handleClose}>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button <Button
size='sm' size="sm"
variant="solid" variant="solid"
loading={loading} loading={loading}
disabled={!newName.trim()} disabled={!newName.trim()}
@ -239,15 +243,10 @@ export const DeleteConfirmModal = forwardRef<HTMLDivElement, DeleteConfirmModalP
</div> </div>
<div className="flex justify-end space-x-2 pt-4 border-t"> <div className="flex justify-end space-x-2 pt-4 border-t">
<Button size='sm' variant="default" disabled={loading} onClick={onClose}> <Button size="sm" variant="default" disabled={loading} onClick={onClose}>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button <Button size="sm" variant="solid" loading={loading} onClick={handleDelete}>
size='sm'
variant="solid"
loading={loading}
onClick={handleDelete}
>
{translate('::Delete')} {translate('::Delete')}
</Button> </Button>
</div> </div>

View file

@ -651,6 +651,7 @@ function OrgChartTree({
setPositions: Dispatch<SetStateAction<PositionMap>> setPositions: Dispatch<SetStateAction<PositionMap>>
edges: EdgeLink[] edges: EdgeLink[]
}) { }) {
const { translate } = useLocalization()
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const nodeRefs = useRef<NodeRefMap>({}) const nodeRefs = useRef<NodeRefMap>({})
const cardRefs = useRef<CardRefMap>({}) const cardRefs = useRef<CardRefMap>({})
@ -765,7 +766,7 @@ function OrgChartTree({
return ( return (
<div className="flex flex-col items-center justify-center py-24 text-slate-400"> <div className="flex flex-col items-center justify-center py-24 text-slate-400">
<FaSitemap className="w-12 h-12 mb-4 opacity-30" /> <FaSitemap className="w-12 h-12 mb-4 opacity-30" />
<p className="text-sm">Veri bulunamadı</p> <p className="text-sm">{translate('::App.NoDataFound')}</p>
</div> </div>
) )
} }
@ -874,7 +875,7 @@ const OrgChart = () => {
return ( return (
<> <>
<PageTitle title="Organizasyon Şeması" /> <PageTitle title={translate('::App.Definitions.OrgChart')} />
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Toolbar */} {/* Toolbar */}
@ -882,7 +883,7 @@ const OrgChart = () => {
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
{MenuIcon} {MenuIcon}
<h4 className="text-sm font-medium truncate text-gray-900 dark:text-white"> <h4 className="text-sm font-medium truncate text-gray-900 dark:text-white">
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'} {translate('::App.Definitions.OrgChart')}
</h4> </h4>
</div> </div>
@ -900,9 +901,7 @@ const OrgChart = () => {
}`} }`}
onClick={() => setMode('department')} onClick={() => setMode('department')}
> >
<span className="hidden sm:inline"> <span className="hidden sm:inline">{translate('::App.Hr.Department')}</span>
{translate('::App.Hr.Department') || 'Departman'}
</span>
</Button> </Button>
<Button <Button
variant="plain" variant="plain"
@ -916,9 +915,7 @@ const OrgChart = () => {
}`} }`}
onClick={() => setMode('jobPosition')} onClick={() => setMode('jobPosition')}
> >
<span className="hidden sm:inline"> <span className="hidden sm:inline">{translate('::App.Hr.JobPosition')}</span>
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
</span>
</Button> </Button>
</div> </div>
@ -931,7 +928,7 @@ const OrgChart = () => {
onChange={(e) => setShowUsers(e.target.checked)} onChange={(e) => setShowUsers(e.target.checked)}
/> />
<span className="hidden sm:inline"> <span className="hidden sm:inline">
{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'} {translate('::App.Definitions.OrgChart.ShowUsers')}
</span> </span>
</label> </label>
</div> </div>
@ -972,10 +969,12 @@ const OrgChart = () => {
shape="none" shape="none"
icon={<FaFileImage className="h-3.5 w-3.5 flex-shrink-0" />} icon={<FaFileImage className="h-3.5 w-3.5 flex-shrink-0" />}
className="!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-lg bg-slate-100 !p-1.5 text-xs font-medium text-slate-600 transition-colors hover:!bg-slate-200 disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700 sm:!p-2 sm:text-sm" className="!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-lg bg-slate-100 !p-1.5 text-xs font-medium text-slate-600 transition-colors hover:!bg-slate-200 disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700 sm:!p-2 sm:text-sm"
title="JPG olarak indir" title={translate('::App.Definitions.OrgChart.ExportJpg')}
onClick={handleExportJpg} onClick={handleExportJpg}
> >
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span> <span className="hidden sm:inline">
{exporting ? translate('::App.Platform.Processing') : 'Export'}
</span>
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -93,7 +93,7 @@ function ChartTabSeries(props: FormEditProps & { listFormCode: string }) {
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {

View file

@ -143,8 +143,8 @@ function FormCustomization({
validate={(values) => { validate={(values) => {
if (values.userId && values.roleId) { if (values.userId && values.roleId) {
return { return {
userId: 'Hem rol hem user seçilemez', userId: translate('::ListForms.RoleOrUserOnly'),
roleId: 'Hem rol hem user seçilemez', roleId: translate('::ListForms.RoleOrUserOnly'),
} }
} }
@ -177,7 +177,7 @@ function FormCustomization({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -312,7 +312,7 @@ function FormCustomization({
> >
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<div className="text-right mt-6"> <div className="text-right mt-6">
<Button <Button

View file

@ -215,7 +215,7 @@ const FormEdit = () => {
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -240,8 +240,7 @@ const FormEdit = () => {
{listFormValues.isTenant && ( {listFormValues.isTenant && (
<Badge <Badge
className="font-semibold dark:bg-red-500 dark:text-white" className="font-semibold dark:bg-red-500 dark:text-white"
content="Bu bir MULTI TENANT form'dur, veri kaybı olmaması için, sorgularda TENANTID content={translate('::ListForms.ListFormEdit.MultiTenantWarning')}
parametresini kullanmayı unutmayınız."
innerClass="p-1 bg-red-50 text-red-500" innerClass="p-1 bg-red-50 text-red-500"
></Badge> ></Badge>
)} )}

View file

@ -103,13 +103,25 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
if (selectCommandColumns.length > 0) return if (selectCommandColumns.length > 0) return
const table = dbObjects.tables.find((t) => t.tableName === cmd) const table = dbObjects.tables.find((t) => t.tableName === cmd)
if (table) { loadColumns(dsCode, table.schemaName, table.tableName); return } if (table) {
loadColumns(dsCode, table.schemaName, table.tableName)
return
}
const view = dbObjects.views.find((v) => v.objectName === cmd) const view = dbObjects.views.find((v) => v.objectName === cmd)
if (view) { loadColumns(dsCode, view.schemaName, view.objectName); return } if (view) {
loadColumns(dsCode, view.schemaName, view.objectName)
return
}
const fn = dbObjects.functions.find((f) => f.objectName === cmd) const fn = dbObjects.functions.find((f) => f.objectName === cmd)
if (fn) { loadColumns(dsCode, fn.schemaName, fn.objectName); return } if (fn) {
loadColumns(dsCode, fn.schemaName, fn.objectName)
return
}
const sp = dbObjects.storedProcedures.find((p) => p.objectName === cmd) const sp = dbObjects.storedProcedures.find((p) => p.objectName === cmd)
if (sp) { loadColumns(dsCode, sp.schemaName, sp.objectName); return } if (sp) {
loadColumns(dsCode, sp.schemaName, sp.objectName)
return
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dbObjects]) }, [dbObjects])
@ -295,7 +307,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
form.setFieldValue('keyFieldName', '') form.setFieldValue('keyFieldName', '')
form.setFieldValue('keyFieldDbSourceType', undefined) form.setFieldValue('keyFieldDbSourceType', undefined)
if (!option.__isNew__ && option.__schema != null && option.__rawName) { if (!option.__isNew__ && option.__schema != null && option.__rawName) {
loadColumns(values.dataSourceCode ?? '', option.__schema, option.__rawName) loadColumns(
values.dataSourceCode ?? '',
option.__schema,
option.__rawName,
)
} else { } else {
setSelectCommandColumns([]) setSelectCommandColumns([])
} }
@ -338,8 +354,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
</span> </span>
) : selectCommandColumns.length === 0 && !isLoadingColumns ? ( ) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
<span className="text-xs ml-2 text-gray-400"> <span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand') + {translate('::ListForms.ListFormEdit.ColumnsLoadAfterSelect', {
' seçince sütunlar yüklenir'} command: translate(
'::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand',
),
})}
</span> </span>
) : null ) : null
} }
@ -355,9 +374,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
placeholder={ placeholder={
isLoadingColumns isLoadingColumns
? translate('::Loading') ? translate('::Loading')
: translate( : translate('::App.Listform.ListformField.KeyFieldName')
'::App.Listform.ListformField.KeyFieldName',
)
} }
options={selectCommandColumns.map((c) => ({ options={selectCommandColumns.map((c) => ({
label: `${c.columnName} (${c.dataType})`, label: `${c.columnName} (${c.dataType})`,

View file

@ -53,7 +53,7 @@ function FormTabGantt(props: FormEditProps) {
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {

View file

@ -52,7 +52,7 @@ function FormTabScheduler(props: FormEditProps) {
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {

View file

@ -107,7 +107,7 @@ function FormTabSubForm() {
<Td>{row.tabTitle}</Td> <Td>{row.tabTitle}</Td>
<Td>{row.tabType}</Td> <Td>{row.tabType}</Td>
<Td>{row.code}</Td> <Td>{row.code}</Td>
<Td>{row.isRefresh ? 'Evet' : 'Hayır'}</Td> <Td>{translate(row.isRefresh ? '::App.Platform.Yes' : '::App.Platform.No')}</Td>
<Td> <Td>
<ul className="list-disc"> <ul className="list-disc">
{row.relation?.map((i) => ( {row.relation?.map((i) => (

View file

@ -50,7 +50,7 @@ function FormTabTree(props: FormEditProps) {
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {
@ -184,7 +184,14 @@ function FormTabTree(props: FormEditProps) {
component={Checkbox} component={Checkbox}
/> />
</FormItem> </FormItem>
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit" className="my-2"> <Button
block
size="sm"
variant="solid"
loading={isSubmitting}
type="submit"
className="my-2"
>
{isSubmitting ? translate('::Saving') : translate('::Save')} {isSubmitting ? translate('::Saving') : translate('::Save')}
</Button> </Button>
</FormContainer> </FormContainer>

View file

@ -146,7 +146,7 @@ function FormFieldTabConditionalFormatting({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -206,7 +206,7 @@ function FormFieldTabConditionalFormatting({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },

View file

@ -30,7 +30,7 @@ const schema = object().shape({
.nullable() .nullable()
.test( .test(
'is-json-object', 'is-json-object',
'editorOptions geçerli bir JSON nesnesi olmalıdır.', 'ListForms.ListFormFieldEdit.EditorOptionsMustBeJsonObject',
(value) => !value?.trim() || isValidJsonText(value), (value) => !value?.trim() || isValidJsonText(value),
), ),
}) })
@ -126,21 +126,15 @@ function FormFieldTabDetails({
/> />
</FormItem> </FormItem>
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsAlignment')}> <FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsAlignment')}>
<SelectField <SelectField name="alignment" options={listFormAlignmentOptions} />
name="alignment"
options={listFormAlignmentOptions}
/>
</FormItem> </FormItem>
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsFormat')}> <FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsFormat')}>
<SelectField <SelectField name="format" options={tooltipFormatListOptions} />
name="format"
options={tooltipFormatListOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
label={translate('::ListForms.ListFormFieldEdit.DetailsEditorOptions')} label={translate('::ListForms.ListFormFieldEdit.DetailsEditorOptions')}
invalid={errors.editorOptions && touched.editorOptions} invalid={errors.editorOptions && touched.editorOptions}
errorMessage={errors.editorOptions} errorMessage={errors.editorOptions && translate('::' + errors.editorOptions)}
> >
<div className="flex gap-2 items-start"> <div className="flex gap-2 items-start">
<Field <Field

View file

@ -78,7 +78,7 @@ function TablePickerModal({
)} )}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200"> <span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{step === 'table' {step === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç' ? translate('::ListForms.Wizard.Step3.SelectTable')
: (pickerTable?.tableName ?? '')} : (pickerTable?.tableName ?? '')}
</span> </span>
</div> </div>
@ -99,7 +99,7 @@ function TablePickerModal({
<input <input
autoFocus autoFocus
value={tableSearch} value={tableSearch}
placeholder={translate('::Search') || 'Ara...'} placeholder={translate('::Search')}
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
onChange={(e) => setTableSearch(e.target.value)} onChange={(e) => setTableSearch(e.target.value)}
/> />
@ -107,7 +107,7 @@ function TablePickerModal({
<div className="overflow-y-auto flex-1 p-2"> <div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? ( {!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6"> <div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') || 'Tablo bulunamadı'} {translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
</div> </div>
) : ( ) : (
dbObjects.tables dbObjects.tables
@ -160,14 +160,14 @@ function TablePickerModal({
<> <>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu {translate('::ListForms.ListFormFieldEdit.KeyColumn')}
</label> </label>
<select <select
value={keyCol} value={keyCol}
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
onChange={(e) => setKeyCol(e.target.value)} onChange={(e) => setKeyCol(e.target.value)}
> >
<option value="">-- Seçiniz --</option> <option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => ( {pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}> <option key={c.columnName} value={c.columnName}>
{c.columnName} {c.columnName}
@ -177,14 +177,14 @@ function TablePickerModal({
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu {translate('::ListForms.ListFormFieldEdit.NameColumn')}
</label> </label>
<select <select
value={nameCol} value={nameCol}
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
onChange={(e) => setNameCol(e.target.value)} onChange={(e) => setNameCol(e.target.value)}
> >
<option value="">-- Seçiniz --</option> <option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => ( {pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}> <option key={c.columnName} value={c.columnName}>
{c.columnName} {c.columnName}
@ -305,7 +305,7 @@ function FormFieldTabLookup({
className="ml-2 !h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40" className="ml-2 !h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40"
onClick={openTablePicker} onClick={openTablePicker}
> >
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'} {translate('::ListForms.Wizard.Step3.GenerateFromTable')}
</Button> </Button>
} }
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery} invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
@ -430,7 +430,14 @@ function FormFieldTabLookup({
</FormItem> </FormItem>
</Card> </Card>
</div> </div>
<Button block size="sm" className="mt-1" variant="solid" loading={isSubmitting} type="submit"> <Button
block
size="sm"
className="mt-1"
variant="solid"
loading={isSubmitting}
type="submit"
>
{isSubmitting ? translate('::Saving') : translate('::Save')} {isSubmitting ? translate('::Saving') : translate('::Save')}
</Button> </Button>
</Form> </Form>

View file

@ -154,7 +154,7 @@ function FormFieldTabValidationRules({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -216,7 +216,7 @@ function FormFieldTabValidationRules({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -235,10 +235,7 @@ function FormFieldTabValidationRules({
invalid={errors.type && touched.type} invalid={errors.type && touched.type}
errorMessage={errors.type} errorMessage={errors.type}
> >
<SelectField <SelectField name="type" options={columnValidationTypeListOptions} />
name="type"
options={columnValidationTypeListOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -328,7 +325,14 @@ function FormFieldTabValidationRules({
<Field name="trim" placeholder="Trim" component={Checkbox} /> <Field name="trim" placeholder="Trim" component={Checkbox} />
</FormItem> </FormItem>
<Button block size="sm" className="mt-1" variant="solid" loading={isLoading} type="submit"> <Button
block
size="sm"
className="mt-1"
variant="solid"
loading={isLoading}
type="submit"
>
{isLoading ? translate('::Saving') : translate('::Save')} {isLoading ? translate('::Saving') : translate('::Save')}
</Button> </Button>
</FormContainer> </FormContainer>

View file

@ -163,7 +163,7 @@ function FormFields({
if (newCols.length === 0) { if (newCols.length === 0) {
toast.push( toast.push(
<Notification type="info" duration={2000}> <Notification type="info" duration={2000}>
Tüm sütunlar zaten eklenmiş. {translate('::ListForms.ListFormField.AllColumnsAlreadyAdded')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -182,7 +182,7 @@ function FormFields({
} }
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{newCols.length} sütun eklendi. {translate('::ListForms.ListFormField.ColumnsAdded', { count: newCols.length })}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -250,7 +250,7 @@ function FormFields({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {
@ -298,7 +298,7 @@ function FormFields({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -324,8 +324,8 @@ function FormFields({
validate={(values) => { validate={(values) => {
if (values.userId && values.roleId) { if (values.userId && values.roleId) {
return { return {
userId: 'Hem rol hem user seçilemez', userId: translate('::ListForms.RoleOrUserOnly'),
roleId: 'Hem rol hem user seçilemez', roleId: translate('::ListForms.RoleOrUserOnly'),
} }
} }
@ -336,7 +336,7 @@ function FormFields({
getFields(cultureName, roleId, userId).then(() => { getFields(cultureName, roleId, userId).then(() => {
toast.push( toast.push(
<Notification type="success" duration={100}> <Notification type="success" duration={100}>
{'Alanlar geldi'} {translate('::ListForms.ListFormField.FieldsLoaded')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -419,7 +419,7 @@ function FormFields({
variant="plain" variant="plain"
type="button" type="button"
size="sm" size="sm"
title="Tüm Sütunları Ekle" title={translate('::ListForms.ListFormField.AddAllColumns')}
icon={<FaTable />} icon={<FaTable />}
loading={isAddingAllColumns} loading={isAddingAllColumns}
onClick={addAllColumns} onClick={addAllColumns}
@ -517,8 +517,8 @@ function FormFields({
validate={(values) => { validate={(values) => {
if (values.userId && values.roleId) { if (values.userId && values.roleId) {
return { return {
userId: 'Hem rol hem user seçilemez', userId: translate('::ListForms.RoleOrUserOnly'),
roleId: 'Hem rol hem user seçilemez', roleId: translate('::ListForms.RoleOrUserOnly'),
} }
} }
@ -547,7 +547,7 @@ function FormFields({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -582,10 +582,7 @@ function FormFields({
</FormItem> </FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.CustomValueType')}> <FormItem label={translate('::ListForms.ListFormEdit.CustomValueType')}>
<SelectField <SelectField name="sourceDbType" options={dbSourceTypeOptions} />
name="sourceDbType"
options={dbSourceTypeOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -594,10 +591,7 @@ function FormFields({
errorMessage={errors.cultureName} errorMessage={errors.cultureName}
asterisk={true} asterisk={true}
> >
<SelectField <SelectField name="cultureName" options={langOptions} />
name="cultureName"
options={langOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -605,10 +599,7 @@ function FormFields({
invalid={errors.roleId && touched.roleId} invalid={errors.roleId && touched.roleId}
errorMessage={errors.roleId} errorMessage={errors.roleId}
> >
<SelectField <SelectField name="roleId" options={roleList} />
name="roleId"
options={roleList}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -616,10 +607,7 @@ function FormFields({
invalid={errors.userId && touched.userId} invalid={errors.userId && touched.userId}
errorMessage={errors.userId} errorMessage={errors.userId}
> >
<SelectField <SelectField name="userId" options={userList} />
name="userId"
options={userList}
/>
</FormItem> </FormItem>
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button block size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -637,8 +625,8 @@ function FormFields({
onClose={() => setDeleteRowId(null)} onClose={() => setDeleteRowId(null)}
onRequestClose={() => setDeleteRowId(null)} onRequestClose={() => setDeleteRowId(null)}
> >
<h5 className="mb-4">Sil</h5> <h5 className="mb-4">{translate('::App.Platform.Delete')}</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<div className="text-right mt-6"> <div className="text-right mt-6">
<Button <Button
className="ltr:mr-2 rtl:ml-2" className="ltr:mr-2 rtl:ml-2"
@ -714,14 +702,19 @@ function FormFields({
try { try {
await postListFormFieldCopy({ ...values }) await postListFormFieldCopy({ ...values })
toast.push(<Notification type="success">{'Field Kopyalandı.'}</Notification>, { toast.push(
<Notification type="success">
{translate('::ListForms.ListFormField.FieldCopied')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleCopyFieldFormClose() handleCopyFieldFormClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -739,10 +732,7 @@ function FormFields({
invalid={errors.sourceFieldName && touched.sourceFieldName} invalid={errors.sourceFieldName && touched.sourceFieldName}
errorMessage={errors.sourceFieldName} errorMessage={errors.sourceFieldName}
> >
<SelectField <SelectField name="sourceFieldName" options={fieldList} />
name="sourceFieldName"
options={fieldList}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -764,10 +754,7 @@ function FormFields({
invalid={errors.sourceCultureName && touched.sourceCultureName} invalid={errors.sourceCultureName && touched.sourceCultureName}
errorMessage={errors.sourceCultureName} errorMessage={errors.sourceCultureName}
> >
<SelectField <SelectField name="sourceCultureName" options={langOptions} />
name="sourceCultureName"
options={langOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -775,10 +762,7 @@ function FormFields({
invalid={errors.targetCultureName && touched.targetCultureName} invalid={errors.targetCultureName && touched.targetCultureName}
errorMessage={errors.targetCultureName} errorMessage={errors.targetCultureName}
> >
<SelectField <SelectField name="targetCultureName" options={langOptions} />
name="targetCultureName"
options={langOptions}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -786,10 +770,7 @@ function FormFields({
invalid={errors.roleId && touched.roleId} invalid={errors.roleId && touched.roleId}
errorMessage={errors.roleId} errorMessage={errors.roleId}
> >
<SelectField <SelectField name="roleId" options={roleList} />
name="roleId"
options={roleList}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -797,10 +778,7 @@ function FormFields({
invalid={errors.userId && touched.userId} invalid={errors.userId && touched.userId}
errorMessage={errors.userId} errorMessage={errors.userId}
> >
<SelectField <SelectField name="userId" options={userList} />
name="userId"
options={userList}
/>
</FormItem> </FormItem>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<FormItem label={translate('::App.Reports.Search')} labelClass="justify-center"> <FormItem label={translate('::App.Reports.Search')} labelClass="justify-center">

View file

@ -248,7 +248,8 @@ function EditorOptionsBuilderDialog({
const visiblePresets = useMemo( const visiblePresets = useMemo(
() => () =>
editorOptionsPresets.filter( editorOptionsPresets.filter(
(preset) => showAll || !preset.editors || !editorType || preset.editors.includes(editorType), (preset) =>
showAll || !preset.editors || !editorType || preset.editors.includes(editorType),
), ),
[editorType, showAll], [editorType, showAll],
) )
@ -326,9 +327,9 @@ function EditorOptionsBuilderDialog({
<h5 className="mr-1">Editor Options</h5> <h5 className="mr-1">Editor Options</h5>
<span <span
className="rounded-full bg-indigo-100 px-2.5 py-[2px] text-[11px] font-medium text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200" className="rounded-full bg-indigo-100 px-2.5 py-[2px] text-[11px] font-medium text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200"
title="Alanlar bu editör tipine göre süzülüyor. Editör tipi Seçenekler sekmesinden değiştirilir." title={translate('::ListForms.EditorOptions.EditorTypeFilterHint')}
> >
{editorType || 'editör tipi seçilmemiş'} {editorType || translate('::ListForms.EditorOptions.NoEditorType')}
</span> </span>
<span <span
className={`rounded-full px-2.5 py-[2px] text-[11px] font-medium ${ className={`rounded-full px-2.5 py-[2px] text-[11px] font-medium ${
@ -336,9 +337,9 @@ function EditorOptionsBuilderDialog({
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200'
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
}`} }`}
title="JSON'a yazılacak ayar sayısı" title={translate('::ListForms.EditorOptions.SetCountHint')}
> >
{setCount} ayar {translate('::ListForms.EditorOptions.SettingCount', { count: setCount })}
</span> </span>
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
@ -346,11 +347,11 @@ function EditorOptionsBuilderDialog({
<button <button
disabled={!!rawError} disabled={!!rawError}
type="button" type="button"
title={rawError ? 'Önce JSON hatasını düzelt.' : undefined} title={rawError ? translate('::ListForms.EditorOptions.FixJsonFirst') : undefined}
className={tabButtonClass(tab === 'builder')} className={tabButtonClass(tab === 'builder')}
onClick={() => goToTab('builder')} onClick={() => goToTab('builder')}
> >
<FaSlidersH /> Ayarlar <FaSlidersH /> {translate('::ListForms.EditorOptions.SettingsTab')}
</button> </button>
<button <button
type="button" type="button"
@ -365,19 +366,18 @@ function EditorOptionsBuilderDialog({
{rawError && ( {rawError && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
JSON geçersiz: {rawError} düzeltilene kadar kaydedilemez. Mevcut değer korunuyor. {translate('::ListForms.EditorOptions.InvalidJsonBanner', {
error: translate('::' + rawError),
})}
</div> </div>
)} )}
{tab === 'json' ? ( {tab === 'json' ? (
<div className="flex min-h-0 flex-1 flex-col gap-2"> <div className="flex min-h-0 flex-1 flex-col gap-2">
<div className="flex items-center justify-between gap-2 text-xs text-gray-500"> <div className="flex items-center justify-between gap-2 text-xs text-gray-500">
<span> <span>{translate('::ListForms.EditorOptions.JsonTabHint')}</span>
Buradaki JSON doğrudan editorOptions alanına yazılır. Geçerli olduğu anda Ayarlar
sekmesi de güncellenir.
</span>
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyPreview}> <Button size="sm" type="button" icon={<FaCopy />} onClick={copyPreview}>
{copied ? 'Kopyalandı' : 'Kopyala'} {translate(copied ? '::App.Platform.Copied' : '::App.SqlQueryManager.Copy')}
</Button> </Button>
</div> </div>
<textarea <textarea
@ -397,47 +397,47 @@ function EditorOptionsBuilderDialog({
<input <input
className={`${controlClass} !w-64`} className={`${controlClass} !w-64`}
value={search} value={search}
placeholder="Ayar ara (mask, format, search…)" placeholder={translate('::ListForms.EditorOptions.SearchSetting')}
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
/> />
<label <label
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300" className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
title="Kapalıyken yalnızca seçili editöre uyan ve halihazırda dolu olan ayarlar listelenir." title={translate('::ListForms.EditorOptions.ShowAllHint')}
> >
<input <input
type="checkbox" type="checkbox"
checked={showAll} checked={showAll}
onChange={(event) => setShowAll(event.target.checked)} onChange={(event) => setShowAll(event.target.checked)}
/> />
Tüm ayarlar {translate('::ListForms.EditorOptions.AllSettings')}
</label> </label>
<label <label
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300" className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
title="Yalnızca JSON'da değeri olan ayarları göster." title={translate('::ListForms.EditorOptions.OnlySetHint')}
> >
<input <input
type="checkbox" type="checkbox"
checked={onlySet} checked={onlySet}
onChange={(event) => setOnlySet(event.target.checked)} onChange={(event) => setOnlySet(event.target.checked)}
/> />
Sadece dolu {translate('::ListForms.EditorOptions.OnlySet')}
</label> </label>
<div className="ml-auto flex items-center gap-1"> <div className="ml-auto flex items-center gap-1">
<Button size="sm" type="button" onClick={() => setAllSections(true)}> <Button size="sm" type="button" onClick={() => setAllSections(true)}>
Tümünü {translate('::ListForms.EditorOptions.ExpandAll')}
</Button> </Button>
<Button size="sm" type="button" onClick={() => setAllSections(false)}> <Button size="sm" type="button" onClick={() => setAllSections(false)}>
Tümünü kapat {translate('::ListForms.EditorOptions.CollapseAll')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
type="button" type="button"
title="Tüm ayarları temizler. Kaydedilirse editorOptions boşalır." title={translate('::ListForms.EditorOptions.ClearAllHint')}
icon={<FaEraser />} icon={<FaEraser />}
onClick={() => setOptions({})} onClick={() => setOptions({})}
> >
Temizle {translate('::App.DeveloperKit.CrudEndpoints.Clear')}
</Button> </Button>
</div> </div>
</div> </div>
@ -450,8 +450,8 @@ function EditorOptionsBuilderDialog({
<div className={cardClass}> <div className={cardClass}>
<SectionHeader <SectionHeader
icon={FaMagic} icon={FaMagic}
title="Hazır Ayarlar" title={translate('::ListForms.EditorOptions.Presets')}
description="Seçilen kalıp mevcut JSON ile birleştirilir; diğer ayarlarını silmez." description={translate('::ListForms.EditorOptions.PresetsHint')}
badge={`${visiblePresets.length}`} badge={`${visiblePresets.length}`}
open={isSectionOpen(PRESETS_KEY)} open={isSectionOpen(PRESETS_KEY)}
onToggle={() => toggleSection(PRESETS_KEY)} onToggle={() => toggleSection(PRESETS_KEY)}
@ -463,10 +463,10 @@ function EditorOptionsBuilderDialog({
key={preset.key} key={preset.key}
size="sm" size="sm"
type="button" type="button"
title={preset.description} title={translate('::' + preset.description)}
onClick={() => setOptions((current) => ({ ...current, ...preset.value }))} onClick={() => setOptions((current) => ({ ...current, ...preset.value }))}
> >
{preset.label} {translate('::' + preset.label)}
</Button> </Button>
))} ))}
</div> </div>
@ -480,15 +480,17 @@ function EditorOptionsBuilderDialog({
<SectionHeader <SectionHeader
accent={group.setCount > 0} accent={group.setCount > 0}
icon={groupIcons[group.key]} icon={groupIcons[group.key]}
title={group.title} title={translate('::' + group.title)}
description={group.description} description={translate('::' + group.description)}
badge={`${group.setCount}/${group.specs.length}`} badge={`${group.setCount}/${group.specs.length}`}
open={open} open={open}
onToggle={() => toggleSection(group.key)} onToggle={() => toggleSection(group.key)}
/> />
{open && ( {open && (
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800"> <div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
<p className="mb-3 text-[11px] text-gray-400">{group.description}</p> <p className="mb-3 text-[11px] text-gray-400">
{translate('::' + group.description)}
</p>
<div className="grid grid-cols-1 gap-x-4 gap-y-3 md:grid-cols-3"> <div className="grid grid-cols-1 gap-x-4 gap-y-3 md:grid-cols-3">
{group.specs.map((spec) => ( {group.specs.map((spec) => (
<OptionField <OptionField
@ -507,8 +509,7 @@ function EditorOptionsBuilderDialog({
{!groupedSpecs.length && ( {!groupedSpecs.length && (
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700"> <div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
Filtreye uyan ayar yok. &quot;Tüm ayarlar&quot; kutusunu işaretleyerek tüm {translate('::ListForms.EditorOptions.NoMatchingSetting')}
sözlüğü görebilirsin.
</div> </div>
)} )}
@ -517,8 +518,8 @@ function EditorOptionsBuilderDialog({
<SectionHeader <SectionHeader
accent={unmanagedPaths.length > 0} accent={unmanagedPaths.length > 0}
icon={FaPuzzlePiece} icon={FaPuzzlePiece}
title="Sözlükte Olmayan Ayarlar" title={translate('::ListForms.EditorOptions.UnmanagedSettings')}
description="JSON'da bulunan ama listede karşılığı olmayan her şey burada görünür." description={translate('::ListForms.EditorOptions.UnmanagedSettingsHint')}
badge={`${unmanagedPaths.length}`} badge={`${unmanagedPaths.length}`}
open={isSectionOpen(UNMANAGED_KEY)} open={isSectionOpen(UNMANAGED_KEY)}
onToggle={() => toggleSection(UNMANAGED_KEY)} onToggle={() => toggleSection(UNMANAGED_KEY)}
@ -526,8 +527,7 @@ function EditorOptionsBuilderDialog({
{isSectionOpen(UNMANAGED_KEY) && ( {isSectionOpen(UNMANAGED_KEY) && (
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800"> <div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
<p className="mb-3 text-[11px] text-gray-400"> <p className="mb-3 text-[11px] text-gray-400">
Hiçbir ayar gizli kalmaz. DevExtreme dokümanındaki herhangi bir yolu buradan {translate('::ListForms.EditorOptions.UnmanagedHelp')}
ekleyebilirsin (örn: toolbar.multiline).
</p> </p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{unmanagedPaths.map((path) => { {unmanagedPaths.map((path) => {
@ -573,7 +573,7 @@ function EditorOptionsBuilderDialog({
size="sm" size="sm"
type="button" type="button"
variant="plain" variant="plain"
title="Bu ayarı sil" title={translate('::ListForms.EditorOptions.DeleteSetting')}
icon={<FaTrash />} icon={<FaTrash />}
onClick={() => onClick={() =>
setOptions((previous) => unsetByPath(previous, path)) setOptions((previous) => unsetByPath(previous, path))
@ -604,7 +604,11 @@ function EditorOptionsBuilderDialog({
<input <input
className={`${controlClass} col-span-5`} className={`${controlClass} col-span-5`}
value={newValue} value={newValue}
placeholder={newType === 'boolean' ? 'true / false' : 'değer'} placeholder={
newType === 'boolean'
? 'true / false'
: translate('::ListForms.EditorOptions.Value')
}
onChange={(event) => setNewValue(event.target.value)} onChange={(event) => setNewValue(event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@ -619,7 +623,7 @@ function EditorOptionsBuilderDialog({
size="sm" size="sm"
type="button" type="button"
variant="plain" variant="plain"
title="Ayarı ekle" title={translate('::ListForms.EditorOptions.AddSetting')}
icon={<FaPlus />} icon={<FaPlus />}
onClick={addCustomOption} onClick={addCustomOption}
/> />
@ -635,18 +639,22 @@ function EditorOptionsBuilderDialog({
<div className="mb-3 flex items-center justify-between gap-2"> <div className="mb-3 flex items-center justify-between gap-2">
<span className="flex items-center gap-2 text-sm font-semibold"> <span className="flex items-center gap-2 text-sm font-semibold">
<FaCode className="text-gray-400" /> <FaCode className="text-gray-400" />
JSON Önizleme {translate('::ListForms.EditorOptions.JsonPreview')}
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] font-normal text-gray-500 dark:bg-gray-800 dark:text-gray-300"> <span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] font-normal text-gray-500 dark:bg-gray-800 dark:text-gray-300">
{preview ? `${stringifyOptions(options).length} karakter` : 'boş'} {preview
? translate('::ListForms.EditorOptions.CharCount', {
count: stringifyOptions(options).length,
})
: translate('::ListForms.EditorOptions.Empty')}
</span> </span>
<Button <Button
shape="circle" shape="circle"
size="sm" size="sm"
type="button" type="button"
variant="plain" variant="plain"
title="JSON'u panoya kopyala" title={translate('::ListForms.EditorOptions.CopyJson')}
icon={copied ? <FaCheck /> : <FaCopy />} icon={copied ? <FaCheck /> : <FaCopy />}
onClick={copyPreview} onClick={copyPreview}
/> />

View file

@ -31,8 +31,8 @@ function EditorScriptBuilderDialog({
// sabitlenmezse editör yazarken sıfırlanırdı. // sabitlenmezse editör yazarken sıfırlanırdı.
const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|') const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|')
const dialect = useMemo( const dialect = useMemo(
() => createFormScriptDialect({ currentField, fields: fieldKey.split('|') }), () => createFormScriptDialect({ currentField, fields: fieldKey.split('|'), translate }),
[currentField, fieldKey], [currentField, fieldKey, translate],
) )
return ( return (

View file

@ -147,7 +147,11 @@ function JsonRowOpDialogAnnotation({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -155,7 +159,7 @@ function JsonRowOpDialogAnnotation({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -449,14 +453,19 @@ function JsonRowOpDialogAnnotation({
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -470,7 +479,7 @@ function JsonRowOpDialogAnnotation({
<Form className="flex flex-col h-full"> <Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-2"> <Dialog.Body className="flex flex-col gap-2">
<h5>Delete</h5> <h5>Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
</Dialog.Body> </Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
<Button size="sm" variant="plain" onClick={handleClose}> <Button size="sm" variant="plain" onClick={handleClose}>

View file

@ -162,7 +162,11 @@ function JsonRowOpDialogAxis({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -170,7 +174,7 @@ function JsonRowOpDialogAxis({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -477,21 +481,26 @@ function JsonRowOpDialogAxis({
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<> <>
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -505,7 +514,12 @@ function JsonRowOpDialogAxis({
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">

View file

@ -139,7 +139,11 @@ function JsonRowOpDialogCommand({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -147,7 +151,7 @@ function JsonRowOpDialogCommand({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -366,7 +370,12 @@ function JsonRowOpDialogCommand({
</Tabs> </Tabs>
</div> </div>
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -382,21 +391,26 @@ function JsonRowOpDialogCommand({
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<> <>
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -411,8 +425,13 @@ function JsonRowOpDialogCommand({
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
{ translate('::Cancel') } size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? 'Deleting' : 'Delete'} {isSubmitting ? 'Deleting' : 'Delete'}

View file

@ -121,7 +121,11 @@ function JsonRowOpDialogDatabase({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -129,7 +133,7 @@ function JsonRowOpDialogDatabase({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -239,8 +243,13 @@ function JsonRowOpDialogDatabase({
)} )}
</div> </div>
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
{ translate('::Cancel') } size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::Saving') : translate('::Save')} {isSubmitting ? translate('::Saving') : translate('::Save')}
@ -255,21 +264,26 @@ function JsonRowOpDialogDatabase({
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<> <>
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -284,8 +298,13 @@ function JsonRowOpDialogDatabase({
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
{ translate('::Cancel') } size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? 'Deleting' : 'Delete'} {isSubmitting ? 'Deleting' : 'Delete'}

View file

@ -56,7 +56,11 @@ function JsonRowOpDialogEditForm({
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi</Notification>) toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
)
await handleClose() await handleClose()
} finally { } finally {
setSubmitting(false) setSubmitting(false)
@ -65,24 +69,46 @@ function JsonRowOpDialogEditForm({
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (
<Form> <Form>
<Dialog.Body><p>{translate('::DeleteConfirmation')}</p></Dialog.Body> <Dialog.Body>
<p>{translate('::DeleteConfirmation')}</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2"> <Dialog.Footer className="flex justify-end gap-2">
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button> <Button type="button" variant="plain" onClick={handleClose}>
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Delete')}</Button> {translate('::Cancel')}
</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>
{translate('::Delete')}
</Button>
</Dialog.Footer> </Dialog.Footer>
</Form> </Form>
)} )}
</Formik> </Formik>
) : ( ) : (
<Formik <Formik
initialValues={data.editFormValues ?? { order: 1, itemType: 'group', caption: '', colCount: 1, colSpan: 1 }} initialValues={
data.editFormValues ?? {
order: 1,
itemType: 'group',
caption: '',
colCount: 1,
colSpan: 1,
}
}
validationSchema={schema} validationSchema={schema}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
try { try {
const input: ListFormJsonRowDto = { index: data.index, fieldName: data.tabName, itemEditingForm: values } const input: ListFormJsonRowDto = {
index: data.index,
fieldName: data.tabName,
itemEditingForm: values,
}
if (data.index === -1) await postListFormJsonRow(data.id, input) if (data.index === -1) await postListFormJsonRow(data.id, input)
else await putListFormJsonRow(data.id, input) else await putListFormJsonRow(data.id, input)
toast.push(<Notification type="success">Kayıt kaydedildi</Notification>) toast.push(
<Notification type="success">
{translate('::App.Platform.RecordSaved')}
</Notification>,
)
await handleClose() await handleClose()
} finally { } finally {
setSubmitting(false) setSubmitting(false)
@ -92,19 +118,44 @@ function JsonRowOpDialogEditForm({
{({ errors, touched, isSubmitting }) => ( {({ errors, touched, isSubmitting }) => (
<Form> <Form>
<Dialog.Body className="grid grid-cols-2 gap-3"> <Dialog.Body className="grid grid-cols-2 gap-3">
<FormItem label={translate('::App.Listform.ListformField.Order')} invalid={!!(errors.order && touched.order)} errorMessage={errors.order}> <FormItem
label={translate('::App.Listform.ListformField.Order')}
invalid={!!(errors.order && touched.order)}
errorMessage={errors.order}
>
<Field name="order" type="number" component={Input} /> <Field name="order" type="number" component={Input} />
</FormItem> </FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormItemType')}> <FormItem label={translate('::ListForms.ListFormEdit.EditingFormItemType')}>
<Field name="itemType">{({ field, form }: FieldProps) => <Select options={itemTypeOptions} value={itemTypeOptions.find((option) => option.value === field.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)} />}</Field> <Field name="itemType">
{({ field, form }: FieldProps) => (
<Select
options={itemTypeOptions}
value={itemTypeOptions.find((option) => option.value === field.value)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
className="col-span-2"
label={translate('::ListForms.ListFormEdit.DetailsTitle')}
>
<Field name="caption" component={Input} />
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnCount')}>
<Field name="colCount" type="number" component={Input} />
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnSpan')}>
<Field name="colSpan" type="number" component={Input} />
</FormItem> </FormItem>
<FormItem className="col-span-2" label={translate('::ListForms.ListFormEdit.DetailsTitle')}><Field name="caption" component={Input} /></FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnCount')}><Field name="colCount" type="number" component={Input} /></FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnSpan')}><Field name="colSpan" type="number" component={Input} /></FormItem>
</Dialog.Body> </Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2"> <Dialog.Footer className="flex justify-end gap-2">
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button> <Button type="button" variant="plain" onClick={handleClose}>
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Save')}</Button> {translate('::Cancel')}
</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>
{translate('::Save')}
</Button>
</Dialog.Footer> </Dialog.Footer>
</Form> </Form>
)} )}

View file

@ -96,7 +96,7 @@ function JsonRowOpDialogExtraFilter({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Alanlar getirilemedi {translate('::App.Platform.FieldsFetchFailed')}
{error.toString()} {error.toString()}
</Notification>, </Notification>,
{ {
@ -156,7 +156,11 @@ function JsonRowOpDialogExtraFilter({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -164,7 +168,7 @@ function JsonRowOpDialogExtraFilter({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -286,7 +290,12 @@ function JsonRowOpDialogExtraFilter({
</FormItem> </FormItem>
</div> </div>
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -302,21 +311,26 @@ function JsonRowOpDialogExtraFilter({
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<> <>
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -331,7 +345,12 @@ function JsonRowOpDialogExtraFilter({
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">

View file

@ -94,7 +94,11 @@ function JsonRowOpDialogPane({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -102,7 +106,7 @@ function JsonRowOpDialogPane({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -134,7 +138,7 @@ function JsonRowOpDialogPane({
<Input <Input
{...field} {...field}
type="color" type="color"
className='!h-10 !p-0 !m-0 !border-0' className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)} onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/> />
)} )}
@ -150,7 +154,12 @@ function JsonRowOpDialogPane({
</FormItem> </FormItem>
</div> </div>
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -166,21 +175,26 @@ function JsonRowOpDialogPane({
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<> <>
<h5 className="mb-4">Delete</h5> <h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>{translate('::DeleteConfirmation')}</p>
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -194,7 +208,12 @@ function JsonRowOpDialogPane({
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<div className="text-right mt-4"> <div className="text-right mt-4">
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}> <Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={handleClose}
>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> <Button size="sm" variant="solid" loading={isSubmitting} type="submit">

View file

@ -187,7 +187,11 @@ function JsonRowOpDialogSeries({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -195,7 +199,7 @@ function JsonRowOpDialogSeries({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -301,7 +305,9 @@ function JsonRowOpDialogSeries({
form={form} form={form}
isClearable={true} isClearable={true}
options={fieldList} options={fieldList}
value={fieldList?.find((option) => option.value === values.valueField)} value={fieldList?.find(
(option) => option.value === values.valueField,
)}
menuPlacement="auto" menuPlacement="auto"
maxMenuHeight={150} maxMenuHeight={150}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)}
@ -348,7 +354,9 @@ function JsonRowOpDialogSeries({
form={form} form={form}
isClearable={true} isClearable={true}
options={valueAxisList()} options={valueAxisList()}
value={valueAxisList()?.find((option) => option.value === values.axis)} value={valueAxisList()?.find(
(option) => option.value === values.axis,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)}
/> />
)} )}
@ -367,7 +375,9 @@ function JsonRowOpDialogSeries({
form={form} form={form}
isClearable={true} isClearable={true}
options={valuePaneList()} options={valuePaneList()}
value={valuePaneList()?.find((option) => option.value === values.pane)} value={valuePaneList()?.find(
(option) => option.value === values.pane,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)}
/> />
)} )}
@ -405,7 +415,7 @@ function JsonRowOpDialogSeries({
<Input <Input
{...field} {...field}
type="color" type="color"
className='!h-10 !p-0 !m-0 !border-0' className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)} onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/> />
)} )}
@ -466,7 +476,7 @@ function JsonRowOpDialogSeries({
<Input <Input
{...field} {...field}
type="color" type="color"
className='!h-10 !p-0 !m-0 !border-0' className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)} onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/> />
)} )}
@ -509,7 +519,7 @@ function JsonRowOpDialogSeries({
<Input <Input
{...field} {...field}
type="color" type="color"
className='!h-10 !p-0 !m-0 !border-0' className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)} onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/> />
)} )}
@ -552,14 +562,19 @@ function JsonRowOpDialogSeries({
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },

View file

@ -186,7 +186,11 @@ function JsonRowOpDialogSubForm({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -194,7 +198,7 @@ function JsonRowOpDialogSubForm({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -451,12 +455,16 @@ function JsonRowOpDialogSubForm({
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>) toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },

View file

@ -117,7 +117,11 @@ function JsonRowOpDialogWidget({
} }
toast.push( toast.push(
<Notification type="success"> <Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'} {translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -125,7 +129,7 @@ function JsonRowOpDialogWidget({
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
@ -259,10 +263,7 @@ function JsonRowOpDialogWidget({
invalid={errors.sqlQuery && touched.sqlQuery} invalid={errors.sqlQuery && touched.sqlQuery}
errorMessage={errors.sqlQuery} errorMessage={errors.sqlQuery}
> >
<div <div className="border rounded-lg overflow-hidden" style={{ height: '35vh' }}>
className="border rounded-lg overflow-hidden"
style={{ height: '35vh' }}
>
<SqlEditor <SqlEditor
value={values.sqlQuery || ''} value={values.sqlQuery || ''}
height="35vh" height="35vh"
@ -355,8 +356,7 @@ function JsonRowOpDialogWidget({
FaTruck FaTruck
</div> </div>
<div> <div>
📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown, 📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown, FaArrowTrendUp
FaArrowTrendUp
</div> </div>
<div> <div>
<strong>Settings:</strong> FaCog, FaTools, FaWrench <strong>Settings:</strong> FaCog, FaTools, FaWrench
@ -441,14 +441,19 @@ function JsonRowOpDialogWidget({
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>, { toast.push(
<Notification type="success">
{translate('::App.Platform.RecordDeleted')}
</Notification>,
{
placement: 'bottom-end', placement: 'bottom-end',
}) },
)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata {translate('::App.Platform.Error')}
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },

View file

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { FaExclamationTriangle, FaUndo } from 'react-icons/fa' import { FaExclamationTriangle, FaUndo } from 'react-icons/fa'
import { coerceNumber, coerceSize, leafToText } from './jsonUtils' import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
import type { OptionSpec } from './optionSpecs' import type { OptionSpec } from './optionSpecs'
import { useLocalization } from '@/utils/hooks/useLocalization'
export const controlClass = export const controlClass =
'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60' 'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60'
@ -15,29 +16,52 @@ type OptionFieldProps = {
} }
/** Değerin spec ile uyuşup uyuşmadığını söyler; uyuşmuyorsa alan kilitlenir. */ /** Değerin spec ile uyuşup uyuşmadığını söyler; uyuşmuyorsa alan kilitlenir. */
const detectMismatch = (spec: OptionSpec, value: unknown): string | undefined => { const detectMismatch = (
spec: OptionSpec,
value: unknown,
): { key: string; params?: Record<string, string> } | undefined => {
if (value === undefined || value === null) return undefined if (value === undefined || value === null) return undefined
const isObjectLike = typeof value === 'object' const isObjectLike = typeof value === 'object'
if (spec.type === 'json') return undefined if (spec.type === 'json') return undefined
if (spec.type === 'stringList') { if (spec.type === 'stringList') {
return Array.isArray(value) ? undefined : 'Bu alan dizi bekliyor, mevcut değer dizi değil.' return Array.isArray(value)
? undefined
: { key: '::ListForms.EditorOptions.Mismatch.ExpectedArray' }
} }
if (isObjectLike) { if (isObjectLike) {
return 'Bu yol şu anda nesne/dizi değeri tutuyor. Ham JSON sekmesinden düzenleyin.' return { key: '::ListForms.EditorOptions.Mismatch.ObjectValue' }
} }
if (spec.type === 'number' && typeof value !== 'number') { if (spec.type === 'number' && typeof value !== 'number') {
return `Sayı bekleniyor, mevcut değer ${typeof value}. Yeni değer girildiğinde sayıya çevrilir.` return {
key: '::ListForms.EditorOptions.Mismatch.ExpectedNumber',
params: { type: typeof value },
}
} }
if (spec.type === 'boolean' && typeof value !== 'boolean') { if (spec.type === 'boolean' && typeof value !== 'boolean') {
return `Boolean bekleniyor, mevcut değer ${typeof value}. Listeden seçim yaparsan düzelir.` return {
key: '::ListForms.EditorOptions.Mismatch.ExpectedBoolean',
params: { type: typeof value },
}
} }
return undefined return undefined
} }
const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => { const BooleanControl = ({
const current = value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : '' value,
onChange,
}: {
value: unknown
onChange: (v: unknown) => void
}) => {
const { translate } = useLocalization()
const current =
value === true || value === 'true'
? 'true'
: value === false || value === 'false'
? 'false'
: ''
return ( return (
<select <select
@ -48,7 +72,7 @@ const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unk
onChange(event.target.value === 'true') onChange(event.target.value === 'true')
}} }}
> >
<option value=""> tanımsız </option> <option value="">{translate('::ListForms.EditorOptions.Undefined')}</option>
<option value="true">true</option> <option value="true">true</option>
<option value="false">false</option> <option value="false">false</option>
</select> </select>
@ -64,6 +88,7 @@ const SelectControl = ({
value: unknown value: unknown
onChange: (v: unknown) => void onChange: (v: unknown) => void
}) => { }) => {
const { translate } = useLocalization()
const current = value === undefined || value === null ? '' : String(value) const current = value === undefined || value === null ? '' : String(value)
const known = spec.choices?.some((choice) => String(choice.value) === current) const known = spec.choices?.some((choice) => String(choice.value) === current)
@ -78,18 +103,23 @@ const SelectControl = ({
onChange(choice ? choice.value : raw) onChange(choice ? choice.value : raw)
}} }}
> >
<option value=""> tanımsız </option> <option value="">{translate('::ListForms.EditorOptions.Undefined')}</option>
{spec.choices?.map((choice) => ( {spec.choices?.map((choice) => (
<option key={String(choice.value)} value={String(choice.value)}> <option key={String(choice.value)} value={String(choice.value)}>
{choice.label} {choice.label}
</option> </option>
))} ))}
{current && !known && <option value={current}>{current} (mevcut değer)</option>} {current && !known && (
<option value={current}>
{current} ({translate('::ListForms.EditorOptions.CurrentValue')})
</option>
)}
</select> </select>
) )
} }
const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => { const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
const { translate } = useLocalization()
const serialized = value === undefined ? '' : JSON.stringify(value, null, 2) const serialized = value === undefined ? '' : JSON.stringify(value, null, 2)
const [draft, setDraft] = useState(serialized) const [draft, setDraft] = useState(serialized)
const [invalid, setInvalid] = useState(false) const [invalid, setInvalid] = useState(false)
@ -122,7 +152,11 @@ const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknow
} }
}} }}
/> />
{invalid && <p className="mt-1 text-[11px] text-red-500">Geçersiz JSON, değer kaydedilmedi.</p>} {invalid && (
<p className="mt-1 text-[11px] text-red-500">
{translate('::ListForms.EditorOptions.InvalidJsonNotSaved')}
</p>
)}
</div> </div>
) )
} }
@ -135,11 +169,14 @@ const StringListControl = ({
spec: OptionSpec spec: OptionSpec
value: unknown value: unknown
onChange: (v: unknown) => void onChange: (v: unknown) => void
}) => ( }) => {
const { translate } = useLocalization()
return (
<input <input
className={controlClass} className={controlClass}
value={Array.isArray(value) ? value.join(', ') : leafToText(value)} value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
placeholder={spec.placeholder} placeholder={spec.placeholder ? translate('::' + spec.placeholder) : undefined}
onChange={(event) => { onChange={(event) => {
const items = event.target.value const items = event.target.value
.split(',') .split(',')
@ -148,10 +185,14 @@ const StringListControl = ({
onChange(items.length ? items : undefined) onChange(items.length ? items : undefined)
}} }}
/> />
) )
}
const OptionField = ({ spec, value, onChange }: OptionFieldProps) => { const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
const { translate } = useLocalization()
const mismatch = detectMismatch(spec, value) const mismatch = detectMismatch(spec, value)
const help = spec.help ? translate('::' + spec.help) : undefined
const placeholder = spec.placeholder ? translate('::' + spec.placeholder) : undefined
const isSet = value !== undefined const isSet = value !== undefined
// Nesne/dizi tutan bir yol basit girdiyle düzenlenemez; ham JSON'a yönlendiriyoruz. // Nesne/dizi tutan bir yol basit girdiyle düzenlenemez; ham JSON'a yönlendiriyoruz.
// Basit tip uyuşmazlıkları (metin yazılmış sayı gibi) düzenlenebilir kalır. // Basit tip uyuşmazlıkları (metin yazılmış sayı gibi) düzenlenebilir kalır.
@ -177,7 +218,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
className={controlClass} className={controlClass}
type="number" type="number"
value={typeof value === 'number' ? value : leafToText(value)} value={typeof value === 'number' ? value : leafToText(value)}
placeholder={spec.placeholder} placeholder={placeholder}
onChange={(event) => onChange(coerceNumber(event.target.value))} onChange={(event) => onChange(coerceNumber(event.target.value))}
/> />
) )
@ -186,7 +227,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<input <input
className={controlClass} className={controlClass}
value={leafToText(value)} value={leafToText(value)}
placeholder={spec.placeholder} placeholder={placeholder}
onChange={(event) => onChange(coerceSize(event.target.value))} onChange={(event) => onChange(coerceSize(event.target.value))}
/> />
) )
@ -195,8 +236,10 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<input <input
className={controlClass} className={controlClass}
value={leafToText(value)} value={leafToText(value)}
placeholder={spec.placeholder} placeholder={placeholder}
onChange={(event) => onChange(event.target.value === '' ? undefined : event.target.value)} onChange={(event) =>
onChange(event.target.value === '' ? undefined : event.target.value)
}
/> />
) )
} }
@ -213,14 +256,14 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<div className="mb-1 flex items-center gap-1"> <div className="mb-1 flex items-center gap-1">
<span <span
className={`min-w-0 truncate text-xs ${isSet ? 'font-semibold text-indigo-600 dark:text-indigo-300' : 'text-gray-500'}`} className={`min-w-0 truncate text-xs ${isSet ? 'font-semibold text-indigo-600 dark:text-indigo-300' : 'text-gray-500'}`}
title={spec.help ? `${spec.path}${spec.help}` : spec.path} title={help ? `${spec.path}${help}` : spec.path}
> >
{spec.label} {spec.label}
</span> </span>
{spec.platform && ( {spec.platform && (
<span <span
className="rounded bg-emerald-100 px-1 text-[10px] text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200" className="rounded bg-emerald-100 px-1 text-[10px] text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200"
title="Bu alan backend tarafından tipli olarak okunur; yanlış tipte yazılırsa yok sayılır." title={translate('::ListForms.EditorOptions.PlatformFieldHint')}
> >
platform platform
</span> </span>
@ -229,7 +272,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<button <button
type="button" type="button"
className="ml-auto shrink-0 text-[10px] text-gray-400 hover:text-red-500" className="ml-auto shrink-0 text-[10px] text-gray-400 hover:text-red-500"
title="Bu ayarı JSON'dan kaldır" title={translate('::ListForms.EditorOptions.RemoveFromJson')}
onClick={() => onChange(undefined)} onClick={() => onChange(undefined)}
> >
<FaUndo /> <FaUndo />
@ -240,13 +283,13 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
{mismatch && ( {mismatch && (
<p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600"> <p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600">
<FaExclamationTriangle className="mt-[2px] shrink-0" /> <FaExclamationTriangle className="mt-[2px] shrink-0" />
<span>{mismatch}</span> <span>{translate(mismatch.key, mismatch.params)}</span>
</p> </p>
)} )}
{/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */} {/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */}
{!mismatch && spec.help && ( {!mismatch && help && (
<p className="mt-1 truncate text-[11px] text-gray-400" title={spec.help}> <p className="mt-1 truncate text-[11px] text-gray-400" title={help}>
{spec.help} {help}
</p> </p>
)} )}
</div> </div>

View file

@ -36,7 +36,7 @@ export const parseOptionsJson = (value?: string): ParseResult => {
try { try {
const parsed = JSON.parse(trimmed) const parsed = JSON.parse(trimmed)
if (!isPlainObject(parsed)) { if (!isPlainObject(parsed)) {
return { data: {}, error: 'editorOptions bir JSON nesnesi ({ ... }) olmalıdır.' } return { data: {}, error: 'ListForms.EditorOptions.MustBeJsonObject' }
} }
return { data: parsed } return { data: parsed }
} catch (error) { } catch (error) {

View file

@ -57,41 +57,68 @@ export type OptionGroupKey =
export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [ export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [
{ {
key: 'common', key: 'common',
title: 'Genel Davranış', title: 'ListForms.EditorOptions.Spec.Common.Title',
description: 'Tüm editörlerde geçerli olan durum, yetki ve doğrulama ayarları.', description: 'ListForms.EditorOptions.Spec.Common.Description',
}, },
{ {
key: 'appearance', key: 'appearance',
title: 'Görünüm ve Boyut', title: 'ListForms.EditorOptions.Spec.Appearance.Title',
description: 'Etiket, boyut ve stil ayarları. Boyutlar sayı veya 100% gibi CSS değeri olabilir.', description: 'ListForms.EditorOptions.Spec.Appearance.Description',
},
{
key: 'text',
title: 'Metin / Maske',
description: 'ListForms.EditorOptions.Spec.Text.Description',
},
{
key: 'number',
title: 'ListForms.EditorOptions.Spec.Number.Title',
description: 'ListForms.EditorOptions.Spec.Number.Description',
},
{
key: 'date',
title: 'ListForms.EditorOptions.Spec.Date.Title',
description: 'ListForms.EditorOptions.Spec.Date.Description',
}, },
{ key: 'text', title: 'Metin / Maske', description: 'TextBox, TextArea ve Autocomplete ayarları.' },
{ key: 'number', title: 'Sayı', description: 'NumberBox için sınır, adım ve format ayarları.' },
{ key: 'date', title: 'Tarih / Saat', description: 'DateBox, Calendar ve DateRangeBox ayarları.' },
{ {
key: 'dropdown', key: 'dropdown',
title: 'Açılır Liste / Arama', title: 'ListForms.EditorOptions.Spec.Dropdown.Title',
description: 'SelectBox, Lookup, TagBox ve GridBox için ortak liste ve arama ayarları.', description: 'ListForms.EditorOptions.Spec.Dropdown.Description',
},
{
key: 'tagBox',
title: 'TagBox',
description: 'ListForms.EditorOptions.Spec.TagBox.Description',
}, },
{ key: 'tagBox', title: 'TagBox', description: 'Çoklu seçim editörüne özel ayarlar.' },
{ {
key: 'gridBox', key: 'gridBox',
title: 'GridBox', title: 'GridBox',
description: 'Tablo görünümlü açılır seçici. Bu alanlar backend tarafından GridBoxOptions olarak okunur.', description: 'ListForms.EditorOptions.Spec.GridBox.Description',
}, },
{ {
key: 'image', key: 'image',
title: 'Görsel Yükleme / Önizleme', title: 'ListForms.EditorOptions.Spec.Image.Title',
description: 'ImageUpload ve ImageViewer editörleri için yükleme ve küçük resim ayarları.', description: 'ListForms.EditorOptions.Spec.Image.Description',
},
{
key: 'choice',
title: 'ListForms.EditorOptions.Spec.Choice.Title',
description: 'ListForms.EditorOptions.Spec.Choice.Description',
},
{
key: 'slider',
title: 'ListForms.EditorOptions.Spec.Slider.Title',
description: 'ListForms.EditorOptions.Spec.Slider.Description',
},
{
key: 'html',
title: 'ListForms.EditorOptions.Spec.Html.Title',
description: 'ListForms.EditorOptions.Spec.Html.Description',
}, },
{ key: 'choice', title: 'Onay / Seçim', description: 'CheckBox, Switch, RadioGroup ve ColorBox ayarları.' },
{ key: 'slider', title: 'Kaydırıcı', description: 'Slider ve RangeSlider ayarları.' },
{ key: 'html', title: 'HTML Editör', description: 'dxHtmlEditor araç çubuğu ve görsel ayarları.' },
{ {
key: 'grid', key: 'grid',
title: 'Liste Sütunu Etkisi', title: 'ListForms.EditorOptions.Spec.Grid.Title',
description: description: 'ListForms.EditorOptions.Spec.Grid.Description',
'Bu alanlar düzenleme editörünün yanı sıra listedeki sütun gösterimini de etkiler.',
}, },
] ]
@ -115,8 +142,6 @@ const SLIDER_EDITORS = ['dxSlider', 'dxRangeSlider']
/** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */ /** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */
const NON_INPUT_EDITORS = ['dxCheckBox', 'dxSwitch', 'dxRadioGroup', ...SLIDER_EDITORS] const NON_INPUT_EDITORS = ['dxCheckBox', 'dxSwitch', 'dxRadioGroup', ...SLIDER_EDITORS]
const booleanHelp = 'Boş bırakılırsa JSON\'a yazılmaz ve DevExtreme varsayılanı geçerli olur.'
/* ------------------------------------------------------------------ * /* ------------------------------------------------------------------ *
* Ayar sözlüğü * Ayar sözlüğü
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
@ -128,31 +153,43 @@ export const optionSpecs: OptionSpec[] = [
label: 'readOnly', label: 'readOnly',
type: 'boolean', type: 'boolean',
group: 'common', group: 'common',
help: 'Alan görünür ama değiştirilemez. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.ReadOnly.Help',
}, },
{ {
path: 'disabled', path: 'disabled',
label: 'disabled', label: 'disabled',
type: 'boolean', type: 'boolean',
group: 'common', group: 'common',
help: 'Alan pasifleşir ve forma dahil edilmez. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.Disabled.Help',
}, },
{ {
path: 'visible', path: 'visible',
label: 'visible', label: 'visible',
type: 'boolean', type: 'boolean',
group: 'common', group: 'common',
help: 'Editörü tamamen gizler. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.Visible.Help',
},
{
path: 'hint',
label: 'hint',
type: 'text',
group: 'common',
help: 'ListForms.EditorOptions.Spec.Hint.Help',
},
{
path: 'tabIndex',
label: 'tabIndex',
type: 'number',
group: 'common',
help: 'ListForms.EditorOptions.Spec.TabIndex.Help',
}, },
{ path: 'hint', label: 'hint', type: 'text', group: 'common', help: 'Fare ile üzerine gelince çıkan ipucu.' },
{ path: 'tabIndex', label: 'tabIndex', type: 'number', group: 'common', help: 'Tab ile gezinme sırası.' },
{ {
path: 'valueChangeEvent', path: 'valueChangeEvent',
label: 'valueChangeEvent', label: 'valueChangeEvent',
type: 'text', type: 'text',
group: 'common', group: 'common',
placeholder: 'change / input / keyup', placeholder: 'change / input / keyup',
help: 'Değerin hangi DOM olayında işleneceği. Anlık kaydetme için input.', help: 'ListForms.EditorOptions.Spec.ValueChangeEvent.Help',
}, },
{ {
path: 'validationMessageMode', path: 'validationMessageMode',
@ -183,14 +220,14 @@ export const optionSpecs: OptionSpec[] = [
type: 'text', type: 'text',
group: 'common', group: 'common',
placeholder: 'my-editor-class', placeholder: 'my-editor-class',
help: 'Editör kök elemanına eklenecek CSS sınıfı.', help: 'ListForms.EditorOptions.Spec.ElementAttrClass.Help',
}, },
{ {
path: 'inputAttr.aria-label', path: 'inputAttr.aria-label',
label: 'inputAttr.aria-label', label: 'inputAttr.aria-label',
type: 'text', type: 'text',
group: 'common', group: 'common',
help: 'Erişilebilirlik etiketi.', help: 'ListForms.EditorOptions.Spec.InputAttrAriaLabel.Help',
}, },
{ {
path: 'inputAttr.style', path: 'inputAttr.style',
@ -198,7 +235,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'text', type: 'text',
group: 'common', group: 'common',
placeholder: 'text-align: right', placeholder: 'text-align: right',
help: 'Girdi alanına doğrudan inline CSS uygular. Sayısal alanları sağa yaslamak için kullanışlıdır.', help: 'ListForms.EditorOptions.Spec.InputAttrStyle.Help',
}, },
// ── Görünüm ────────────────────────────────────────────────────── // ── Görünüm ──────────────────────────────────────────────────────
@ -208,7 +245,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'size', type: 'size',
group: 'appearance', group: 'appearance',
placeholder: '100% veya 240', placeholder: '100% veya 240',
help: 'Sadece rakam yazarsan sayı olarak kaydedilir (240). Yüzde için 100% yaz.', help: 'ListForms.EditorOptions.Spec.Width.Help',
}, },
{ path: 'height', label: 'height', type: 'size', group: 'appearance', placeholder: '200' }, { path: 'height', label: 'height', type: 'size', group: 'appearance', placeholder: '200' },
{ {
@ -216,9 +253,15 @@ export const optionSpecs: OptionSpec[] = [
label: 'placeholder', label: 'placeholder',
type: 'text', type: 'text',
group: 'appearance', group: 'appearance',
help: 'Alan boşken görünen metin. Sütunun PlaceHolder alanını ezer.', help: 'ListForms.EditorOptions.Spec.Placeholder.Help',
},
{
path: 'label',
label: 'label',
type: 'text',
group: 'appearance',
help: 'ListForms.EditorOptions.Spec.Label.Help',
}, },
{ path: 'label', label: 'label', type: 'text', group: 'appearance', help: 'Editör içi etiket metni.' },
{ {
path: 'labelMode', path: 'labelMode',
label: 'labelMode', label: 'labelMode',
@ -247,7 +290,7 @@ export const optionSpecs: OptionSpec[] = [
label: 'showClearButton', label: 'showClearButton',
type: 'boolean', type: 'boolean',
group: 'appearance', group: 'appearance',
help: 'Temizleme (x) butonu. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.ShowClearButton.Help',
}, },
// ── Metin / Maske ──────────────────────────────────────────────── // ── Metin / Maske ────────────────────────────────────────────────
@ -275,7 +318,14 @@ export const optionSpecs: OptionSpec[] = [
editors: TEXT_EDITORS, editors: TEXT_EDITORS,
placeholder: '(000) 000-0000', placeholder: '(000) 000-0000',
}, },
{ path: 'maskChar', label: 'maskChar', type: 'text', group: 'text', editors: TEXT_EDITORS, placeholder: '_' }, {
path: 'maskChar',
label: 'maskChar',
type: 'text',
group: 'text',
editors: TEXT_EDITORS,
placeholder: '_',
},
{ {
path: 'maskInvalidMessage', path: 'maskInvalidMessage',
label: 'maskInvalidMessage', label: 'maskInvalidMessage',
@ -300,7 +350,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean', type: 'boolean',
group: 'text', group: 'text',
editors: TEXT_EDITORS, editors: TEXT_EDITORS,
help: 'true ise maske karakterleri de veritabanına yazılır. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.UseMaskedValue.Help',
}, },
{ {
path: 'maskRules.X', path: 'maskRules.X',
@ -309,16 +359,22 @@ export const optionSpecs: OptionSpec[] = [
group: 'text', group: 'text',
editors: TEXT_EDITORS, editors: TEXT_EDITORS,
placeholder: '[0-9]', placeholder: '[0-9]',
help: 'Maskede X karakterinin karşılığı olan düzenli ifade.', help: 'ListForms.EditorOptions.Spec.MaskRulesX.Help',
},
{
path: 'spellcheck',
label: 'spellcheck',
type: 'boolean',
group: 'text',
editors: TEXT_EDITORS,
}, },
{ path: 'spellcheck', label: 'spellcheck', type: 'boolean', group: 'text', editors: TEXT_EDITORS },
{ {
path: 'autoResizeEnabled', path: 'autoResizeEnabled',
label: 'autoResizeEnabled', label: 'autoResizeEnabled',
type: 'boolean', type: 'boolean',
group: 'text', group: 'text',
editors: ['dxTextArea'], editors: ['dxTextArea'],
help: 'İçerik büyüdükçe alan uzar. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.AutoResizeEnabled.Help',
}, },
{ path: 'minHeight', label: 'minHeight', type: 'size', group: 'text', editors: ['dxTextArea'] }, { path: 'minHeight', label: 'minHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
{ path: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] }, { path: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
@ -347,7 +403,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'select', type: 'select',
group: 'number', group: 'number',
editors: ['dxNumberBox'], editors: ['dxNumberBox'],
help: 'Sayısal format tipi. Seçilirse format alanı nesne olur.', help: 'ListForms.EditorOptions.Spec.FormatType.Help',
choices: [ choices: [
{ value: 'fixedPoint', label: 'fixedPoint' }, { value: 'fixedPoint', label: 'fixedPoint' },
{ value: 'decimal', label: 'decimal' }, { value: 'decimal', label: 'decimal' },
@ -366,7 +422,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'number', type: 'number',
group: 'number', group: 'number',
editors: ['dxNumberBox'], editors: ['dxNumberBox'],
help: 'Ondalık basamak sayısı.', help: 'ListForms.EditorOptions.Spec.FormatPrecision.Help',
}, },
{ {
path: 'format.currency', path: 'format.currency',
@ -389,7 +445,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean', type: 'boolean',
group: 'number', group: 'number',
editors: ['dxNumberBox', 'dxDateBox'], editors: ['dxNumberBox', 'dxDateBox'],
help: 'Girdiyi formatlı maske olarak yönetir. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.UseMaskBehavior.Help',
}, },
// ── Tarih ──────────────────────────────────────────────────────── // ── Tarih ────────────────────────────────────────────────────────
@ -425,7 +481,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'date', group: 'date',
editors: DATE_EDITORS, editors: DATE_EDITORS,
placeholder: 'dd/MM/yyyy', placeholder: 'dd/MM/yyyy',
help: 'Ekranda gösterilecek biçim. Listedeki sütun biçimini de ezer.', help: 'ListForms.EditorOptions.Spec.DisplayFormat.Help',
}, },
{ {
path: 'dateSerializationFormat', path: 'dateSerializationFormat',
@ -434,7 +490,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'date', group: 'date',
editors: DATE_EDITORS, editors: DATE_EDITORS,
placeholder: 'yyyy-MM-ddTHH:mm:ss', placeholder: 'yyyy-MM-ddTHH:mm:ss',
help: 'Veritabanına yazılacak biçim. Tarih kaymalarının çoğu bu alandan kaynaklanır.', help: 'ListForms.EditorOptions.Spec.DateSerializationFormat.Help',
}, },
{ {
path: 'interval', path: 'interval',
@ -442,11 +498,11 @@ export const optionSpecs: OptionSpec[] = [
type: 'number', type: 'number',
group: 'date', group: 'date',
editors: DATE_EDITORS, editors: DATE_EDITORS,
help: 'Saat listesinde dakika aralığı.', help: 'ListForms.EditorOptions.Spec.Interval.Help',
}, },
{ {
path: 'min', path: 'min',
label: 'min (tarih)', label: 'min',
type: 'text', type: 'text',
group: 'date', group: 'date',
editors: DATE_EDITORS, editors: DATE_EDITORS,
@ -454,7 +510,7 @@ export const optionSpecs: OptionSpec[] = [
}, },
{ {
path: 'max', path: 'max',
label: 'max (tarih)', label: 'max',
type: 'text', type: 'text',
group: 'date', group: 'date',
editors: DATE_EDITORS, editors: DATE_EDITORS,
@ -515,7 +571,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown', group: 'dropdown',
editors: DROPDOWN_EDITORS, editors: DROPDOWN_EDITORS,
platform: true, platform: true,
help: 'Listede arama kutusu. TagBox için backend TagBoxOptions olarak da okur. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.SearchEnabled.Help',
}, },
{ {
path: 'searchMode', path: 'searchMode',
@ -535,7 +591,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown', group: 'dropdown',
editors: DROPDOWN_EDITORS, editors: DROPDOWN_EDITORS,
placeholder: 'name', placeholder: 'name',
help: 'Aramanın hangi alanda yapılacağı.', help: 'ListForms.EditorOptions.Spec.SearchExpr.Help',
}, },
{ {
path: 'searchTimeout', path: 'searchTimeout',
@ -565,7 +621,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown', group: 'dropdown',
editors: DROPDOWN_EDITORS, editors: DROPDOWN_EDITORS,
platform: true, platform: true,
help: 'Listede olmayan değerin yazılmasına izin verir. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.AcceptCustomValue.Help',
}, },
{ {
path: 'noDataText', path: 'noDataText',
@ -573,7 +629,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'text', type: 'text',
group: 'dropdown', group: 'dropdown',
editors: DROPDOWN_EDITORS, editors: DROPDOWN_EDITORS,
placeholder: 'Kayıt bulunamadı', placeholder: 'ListForms.EditorOptions.Spec.NoDataText.Placeholder',
}, },
{ {
path: 'deferRendering', path: 'deferRendering',
@ -581,7 +637,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean', type: 'boolean',
group: 'dropdown', group: 'dropdown',
editors: DROPDOWN_EDITORS, editors: DROPDOWN_EDITORS,
help: 'Liste yalnızca açıldığında render edilir. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.DeferRendering.Help',
}, },
{ {
path: 'wrapItemText', path: 'wrapItemText',
@ -620,7 +676,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'tagBox', group: 'tagBox',
editors: ['dxTagBox'], editors: ['dxTagBox'],
platform: true, platform: true,
help: 'Liste öğelerinde onay kutusu gösterir. Boş bırakılırsa platform varsayılanı true. ', help: 'ListForms.EditorOptions.Spec.ShowSelectionControls.Help',
}, },
{ {
path: 'maxDisplayedTags', path: 'maxDisplayedTags',
@ -645,13 +701,19 @@ export const optionSpecs: OptionSpec[] = [
group: 'tagBox', group: 'tagBox',
editors: ['dxTagBox'], editors: ['dxTagBox'],
platform: true, platform: true,
help: 'Boş bırakılırsa platform varsayılanı useButtons.', help: 'ListForms.EditorOptions.Spec.ApplyValueMode.Help',
choices: [ choices: [
{ value: 'instantly', label: 'instantly' }, { value: 'instantly', label: 'instantly' },
{ value: 'useButtons', label: 'useButtons' }, { value: 'useButtons', label: 'useButtons' },
], ],
}, },
{ path: 'multiline', label: 'multiline', type: 'boolean', group: 'tagBox', editors: ['dxTagBox'] }, {
path: 'multiline',
label: 'multiline',
type: 'boolean',
group: 'tagBox',
editors: ['dxTagBox'],
},
{ {
path: 'hideSelectedItems', path: 'hideSelectedItems',
label: 'hideSelectedItems', label: 'hideSelectedItems',
@ -669,7 +731,7 @@ export const optionSpecs: OptionSpec[] = [
editors: ['dxGridBox'], editors: ['dxGridBox'],
platform: true, platform: true,
placeholder: 'key, name, group', placeholder: 'key, name, group',
help: 'ılır tabloda gösterilecek sütunlar. Virgülle ayır; JSON\'a dizi olarak yazılır.', help: 'ListForms.EditorOptions.Spec.Columns.Help',
}, },
{ {
path: 'selectionMode', path: 'selectionMode',
@ -678,7 +740,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'gridBox', group: 'gridBox',
editors: ['dxGridBox'], editors: ['dxGridBox'],
platform: true, platform: true,
help: 'multiple seçilirse liste sütunu da çoklu değer olarak çalışır.', help: 'ListForms.EditorOptions.Spec.SelectionMode.Help',
choices: [ choices: [
{ value: 'single', label: 'single' }, { value: 'single', label: 'single' },
{ value: 'multiple', label: 'multiple' }, { value: 'multiple', label: 'multiple' },
@ -692,7 +754,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'gridBox', group: 'gridBox',
editors: ['dxGridBox'], editors: ['dxGridBox'],
platform: true, platform: true,
help: 'ılır tabloda filtre satırı. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.FilterRowVisible.Help',
}, },
// ── Görsel ─────────────────────────────────────────────────────── // ── Görsel ───────────────────────────────────────────────────────
@ -703,7 +765,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image', group: 'image',
editors: IMAGE_EDITORS, editors: IMAGE_EDITORS,
platform: true, platform: true,
help: 'Boş bırakılırsa görsel base64 olarak saklanır.', help: 'ListForms.EditorOptions.Spec.UploadUrl.Help',
}, },
{ {
path: 'fileFieldName', path: 'fileFieldName',
@ -712,7 +774,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image', group: 'image',
editors: IMAGE_EDITORS, editors: IMAGE_EDITORS,
placeholder: 'file', placeholder: 'file',
help: 'Yükleme isteğindeki form alan adı.', help: 'ListForms.EditorOptions.Spec.FileFieldName.Help',
}, },
{ {
path: 'accept', path: 'accept',
@ -730,7 +792,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image', group: 'image',
editors: IMAGE_EDITORS, editors: IMAGE_EDITORS,
platform: true, platform: true,
help: 'Çoklu görsel. Metin olarak "true" yazılırsa da çalışır ama boolean tercih edilmeli.', help: 'ListForms.EditorOptions.Spec.Multiple.Help',
}, },
{ {
path: 'maxFileSize', path: 'maxFileSize',
@ -748,10 +810,22 @@ export const optionSpecs: OptionSpec[] = [
type: 'text', type: 'text',
group: 'choice', group: 'choice',
editors: ['dxCheckBox'], editors: ['dxCheckBox'],
help: 'Onay kutusunun yanında görünen metin.', help: 'ListForms.EditorOptions.Spec.Text.Help',
},
{
path: 'switchedOnText',
label: 'switchedOnText',
type: 'text',
group: 'choice',
editors: ['dxSwitch'],
},
{
path: 'switchedOffText',
label: 'switchedOffText',
type: 'text',
group: 'choice',
editors: ['dxSwitch'],
}, },
{ path: 'switchedOnText', label: 'switchedOnText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
{ path: 'switchedOffText', label: 'switchedOffText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
{ {
path: 'layout', path: 'layout',
label: 'layout', label: 'layout',
@ -794,7 +868,13 @@ export const optionSpecs: OptionSpec[] = [
{ value: 'always', label: 'always' }, { value: 'always', label: 'always' },
], ],
}, },
{ path: 'showRange', label: 'showRange', type: 'boolean', group: 'slider', editors: SLIDER_EDITORS }, {
path: 'showRange',
label: 'showRange',
type: 'boolean',
group: 'slider',
editors: SLIDER_EDITORS,
},
// ── HTML editör ────────────────────────────────────────────────── // ── HTML editör ──────────────────────────────────────────────────
{ {
@ -821,7 +901,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'json', type: 'json',
group: 'html', group: 'html',
editors: ['dxHtmlEditor'], editors: ['dxHtmlEditor'],
help: 'Araç çubuğu tanımı. Hazır ayarlardaki "htmlEditor toolbar" bunu doldurur.', help: 'ListForms.EditorOptions.Spec.ToolbarItems.Help',
}, },
{ {
path: 'mediaResizing.enabled', path: 'mediaResizing.enabled',
@ -857,21 +937,21 @@ export const optionSpecs: OptionSpec[] = [
type: 'text', type: 'text',
group: 'grid', group: 'grid',
placeholder: 'dd/MM/yyyy veya currency', placeholder: 'dd/MM/yyyy veya currency',
help: 'Metin olarak yazılırsa listedeki sütun biçimini de ezer. Sayı formatı için Sayı bölümündeki format.type alanını kullan.', help: 'ListForms.EditorOptions.Spec.Format.Help',
}, },
{ {
path: 'encodeHtml', path: 'encodeHtml',
label: 'encodeHtml', label: 'encodeHtml',
type: 'boolean', type: 'boolean',
group: 'grid', group: 'grid',
help: 'false yapılırsa hücre içeriği HTML olarak render edilir. ' + booleanHelp, help: 'ListForms.EditorOptions.Spec.EncodeHtml.Help',
}, },
{ {
path: 'buttons', path: 'buttons',
label: 'buttons', label: 'buttons',
type: 'json', type: 'json',
group: 'grid', group: 'grid',
help: 'Editör içi butonlar. options.onClick metin olarak yazılırsa çalışma anında fonksiyona çevrilir.', help: 'ListForms.EditorOptions.Spec.Buttons.Help',
}, },
] ]

View file

@ -96,32 +96,32 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
{ {
key: 'readOnly', key: 'readOnly',
label: 'Salt okunur', label: 'Salt okunur',
description: 'readOnly: true — alan görünür ama değiştirilemez.', description: 'ListForms.EditorOptions.Preset.ReadOnly.Description',
value: { readOnly: true }, value: { readOnly: true },
}, },
{ {
key: 'disabled', key: 'disabled',
label: 'Pasif', label: 'Pasif',
description: 'disabled: true — alan tamamen pasifleşir.', description: 'ListForms.EditorOptions.Preset.Disabled.Description',
value: { disabled: true }, value: { disabled: true },
}, },
{ {
key: 'rightAligned', key: 'rightAligned',
label: 'Sağa yaslı', label: 'ListForms.EditorOptions.Preset.RightAligned.Label',
description: 'Girdi metnini sağa yaslar. Tutar/miktar alanları için.', description: 'ListForms.EditorOptions.Preset.RightAligned.Description',
value: { inputAttr: { style: 'text-align: right' } }, value: { inputAttr: { style: 'text-align: right' } },
}, },
{ {
key: 'fixedPoint2', key: 'fixedPoint2',
label: 'Ondalık (2 hane)', label: 'ListForms.EditorOptions.Preset.FixedPoint2.Label',
description: 'format.type fixedPoint, precision 2.', description: 'format.type fixedPoint, precision 2.',
editors: ['dxNumberBox'], editors: ['dxNumberBox'],
value: { format: { type: 'fixedPoint', precision: 2 } }, value: { format: { type: 'fixedPoint', precision: 2 } },
}, },
{ {
key: 'numberSpin2', key: 'numberSpin2',
label: 'Ondalık + spin', label: 'ListForms.EditorOptions.Preset.NumberSpin2.Label',
description: 'precision 2, mask davranışı ve artır/azalt butonları.', description: 'ListForms.EditorOptions.Preset.NumberSpin2.Description',
editors: ['dxNumberBox'], editors: ['dxNumberBox'],
value: { value: {
format: { type: 'fixedPoint', precision: 2 }, format: { type: 'fixedPoint', precision: 2 },
@ -132,14 +132,14 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
{ {
key: 'currency', key: 'currency',
label: 'Para birimi (TRY)', label: 'Para birimi (TRY)',
description: 'currency formatı, 2 ondalık.', description: 'ListForms.EditorOptions.Preset.Currency.Description',
editors: ['dxNumberBox'], editors: ['dxNumberBox'],
value: { format: { type: 'currency', precision: 2, currency: 'TRY' } }, value: { format: { type: 'currency', precision: 2, currency: 'TRY' } },
}, },
{ {
key: 'date', key: 'date',
label: 'Tarih dd/MM/yyyy', label: 'ListForms.EditorOptions.Preset.DateFormat.Label',
description: 'Görünüm ve veritabanı biçimini birlikte ayarlar.', description: 'ListForms.EditorOptions.Preset.Date.Description',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'], editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: { value: {
type: 'date', type: 'date',
@ -149,8 +149,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'dateTime', key: 'dateTime',
label: 'Tarih + saat', label: 'ListForms.EditorOptions.Preset.DateTime.Label',
description: 'dd/MM/yyyy HH:mm görünümü, ISO serileştirme.', description: 'ListForms.EditorOptions.Preset.DateTime.Description',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'], editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: { value: {
type: 'datetime', type: 'datetime',
@ -160,8 +160,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'timePicker', key: 'timePicker',
label: 'Saat seçici', label: 'ListForms.EditorOptions.Preset.TimePicker.Label',
description: '5 dakika aralıklı liste tipi saat seçimi.', description: 'ListForms.EditorOptions.Preset.TimePicker.Description',
editors: ['dxDateBox'], editors: ['dxDateBox'],
value: { value: {
type: 'time', type: 'time',
@ -175,7 +175,7 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
{ {
key: 'phoneMask', key: 'phoneMask',
label: 'Telefon maskesi', label: 'Telefon maskesi',
description: 'Maske, yer tutucu ve hata mesajı.', description: 'ListForms.EditorOptions.Preset.PhoneMask.Description',
editors: ['dxTextBox', 'dxAutocomplete'], editors: ['dxTextBox', 'dxAutocomplete'],
value: { value: {
mask: '(000) 000-0000', mask: '(000) 000-0000',
@ -187,22 +187,34 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'textAreaAuto', key: 'textAreaAuto',
label: 'Otomatik büyüyen alan', label: 'ListForms.EditorOptions.Preset.TextAreaAuto.Label',
description: 'autoResizeEnabled ve minimum yükseklik.', description: 'ListForms.EditorOptions.Preset.TextAreaAuto.Description',
editors: ['dxTextArea'], editors: ['dxTextArea'],
value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 }, value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 },
}, },
{ {
key: 'searchableList', key: 'searchableList',
label: 'Aranabilir liste', label: 'Aranabilir liste',
description: 'İçinde geçen kayıtları arar, 300 ms gecikme.', description: 'ListForms.EditorOptions.Preset.SearchableList.Description',
editors: ['dxSelectBox', 'dxLookup', 'dxTagBox', 'dxDropDownBox', 'dxGridBox', 'dxAutocomplete'], editors: [
value: { searchEnabled: true, searchMode: 'contains', searchTimeout: 300, showClearButton: true }, 'dxSelectBox',
'dxLookup',
'dxTagBox',
'dxDropDownBox',
'dxGridBox',
'dxAutocomplete',
],
value: {
searchEnabled: true,
searchMode: 'contains',
searchTimeout: 300,
showClearButton: true,
},
}, },
{ {
key: 'tagBoxInstant', key: 'tagBoxInstant',
label: 'TagBox anlık seçim', label: 'ListForms.EditorOptions.Preset.TagBoxInstant.Label',
description: 'Onay kutulu, butonsuz, anında uygulanan çoklu seçim.', description: 'ListForms.EditorOptions.Preset.TagBoxInstant.Description',
editors: ['dxTagBox'], editors: ['dxTagBox'],
value: { value: {
showSelectionControls: true, showSelectionControls: true,
@ -213,8 +225,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'gridBoxSingle', key: 'gridBoxSingle',
label: 'GridBox tekli seçim', label: 'ListForms.EditorOptions.Preset.GridBoxSingle.Label',
description: 'key/name sütunları, filtre satırıık, tek seçim.', description: 'ListForms.EditorOptions.Preset.GridBoxSingle.Description',
editors: ['dxGridBox'], editors: ['dxGridBox'],
value: { value: {
columns: ['key', 'name'], columns: ['key', 'name'],
@ -227,8 +239,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'gridBoxMulti', key: 'gridBoxMulti',
label: 'GridBox çoklu seçim', label: 'ListForms.EditorOptions.Preset.GridBoxMulti.Label',
description: 'Çoklu seçim; liste sütunu da çoklu değer olarak çalışır.', description: 'ListForms.EditorOptions.Preset.GridBoxMulti.Description',
editors: ['dxGridBox'], editors: ['dxGridBox'],
value: { value: {
columns: ['key', 'name'], columns: ['key', 'name'],
@ -240,27 +252,27 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
}, },
{ {
key: 'imageMulti', key: 'imageMulti',
label: 'Çoklu görsel 80x80', label: 'ListForms.EditorOptions.Preset.ImageMulti.Label',
description: 'Çoklu yükleme ve 80x80 küçük resim.', description: 'ListForms.EditorOptions.Preset.ImageMulti.Description',
editors: ['dxImageUpload', 'dxImageViewer'], editors: ['dxImageUpload', 'dxImageViewer'],
value: { width: 80, height: 80, multiple: true, accept: 'image/*' }, value: { width: 80, height: 80, multiple: true, accept: 'image/*' },
}, },
{ {
key: 'htmlToolbar', key: 'htmlToolbar',
label: 'HtmlEditor araç çubuğu', label: 'ListForms.EditorOptions.Preset.HtmlToolbar.Label',
description: 'Tam araç çubuğu, görsel yükleme ve boyutlandırma.', description: 'ListForms.EditorOptions.Preset.HtmlToolbar.Description',
editors: ['dxHtmlEditor'], editors: ['dxHtmlEditor'],
value: buildHtmlEditorOptions(), value: buildHtmlEditorOptions(),
}, },
{ {
key: 'height100', key: 'height100',
label: 'Yükseklik 100', label: 'ListForms.EditorOptions.Preset.Height100.Label',
description: 'height: 100', description: 'height: 100',
value: { height: 100 }, value: { height: 100 },
}, },
{ {
key: 'height200', key: 'height200',
label: 'Yükseklik 200', label: 'ListForms.EditorOptions.Preset.Height200.Label',
description: 'height: 200', description: 'height: 200',
value: { height: 200 }, value: { height: 200 },
}, },

View file

@ -36,6 +36,7 @@ import {
type RecipeId, type RecipeId,
type RecipeParam, type RecipeParam,
type ScriptRule as FormScriptRule, type ScriptRule as FormScriptRule,
type TranslateFn,
} from './scriptRecipes' } from './scriptRecipes'
const groupIcons: Record<RecipeGroupKey, ScriptRecipeGroup['icon']> = { const groupIcons: Record<RecipeGroupKey, ScriptRecipeGroup['icon']> = {
@ -52,8 +53,7 @@ const FIELD_SOURCE = 'field'
const toParam = (param: RecipeParam): ScriptRecipeParam => ({ const toParam = (param: RecipeParam): ScriptRecipeParam => ({
key: param.key, key: param.key,
label: param.label, label: param.label,
type: type: param.type === 'field' ? 'option' : param.type === 'fieldList' ? 'fieldList' : param.type,
param.type === 'field' ? 'option' : param.type === 'fieldList' ? 'fieldList' : param.type,
optionSource: param.type === 'field' || param.type === 'fieldList' ? FIELD_SOURCE : undefined, optionSource: param.type === 'field' || param.type === 'fieldList' ? FIELD_SOURCE : undefined,
placeholder: param.placeholder, placeholder: param.placeholder,
help: param.help, help: param.help,
@ -79,57 +79,57 @@ const apiReference: ScriptApiEntry[] = runtimeApiReference
*/ */
const snippets: ScriptSnippet[] = [ const snippets: ScriptSnippet[] = [
{ {
label: 'Koşullu değer yaz', label: 'ListForms.ScriptBuilder.Snippet.ConditionalSet.Label',
group: 'Akış', group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
description: 'Bir alan doluysa hedefe yaz.', description: 'ListForms.ScriptBuilder.Snippet.ConditionalSet.Description',
code: "if (get('${1:Kaynak}')) set('${2:Hedef}', get('${1:Kaynak}'))", code: "if (get('${1:Kaynak}')) set('${2:Hedef}', get('${1:Kaynak}'))",
}, },
{ {
label: 'Çok koşullu blok', label: 'ListForms.ScriptBuilder.Snippet.MultiCondition.Label',
group: 'Akış', group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
description: 'VE / VEYA ile birleşen iki koşul.', description: 'ListForms.ScriptBuilder.Snippet.MultiCondition.Description',
code: "if (str('${1:Durum}') === '${2:Aktif}' && num('${3:Tutar}') > ${4:1000}) {\n ${5:notify('Limit aşıldı')}\n}", code: "if (str('${1:Durum}') === '${2:Aktif}' && num('${3:Tutar}') > ${4:1000}) {\n ${5:notify('Limit aşıldı')}\n}",
}, },
{ {
label: 'ılışta / değişimde ayrımı', label: 'ListForms.ScriptBuilder.Snippet.OpenVsChange.Label',
group: 'Akış', group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
description: 'isReady form açılışında true gelir.', description: 'ListForms.ScriptBuilder.Snippet.OpenVsChange.Description',
code: "if (isReady) {\n ${1:// form açılırken}\n} else {\n ${2:// değer değişince}\n}", code: 'if (isReady) {\n ${1:// form açılırken}\n} else {\n ${2:// değer değişince}\n}',
}, },
{ {
label: 'Toplu alan yazma', label: 'ListForms.ScriptBuilder.Snippet.BulkSet.Label',
group: 'Veri', group: 'ListForms.ScriptBuilder.Snippet.Group.Data',
description: 'Tek çağrıda birden fazla alan.', description: 'ListForms.ScriptBuilder.Snippet.BulkSet.Description',
code: "set({ ${1:Alan1}: ${2:null}, ${3:Alan2}: ${4:null} })", code: 'set({ ${1:Alan1}: ${2:null}, ${3:Alan2}: ${4:null} })',
}, },
{ {
label: 'Seçili kayıttan doldur', label: 'ListForms.ScriptBuilder.Snippet.FillFromSelected.Label',
group: 'Veri', group: 'ListForms.ScriptBuilder.Snippet.Group.Data',
description: 'Lookup seçiminden birkaç alanı birden taşı.', description: 'ListForms.ScriptBuilder.Snippet.FillFromSelected.Description',
code: "copy('${1:kolon}', '${2:Hedef}')\ncopy('${3:kolon2}', '${4:Hedef2}')", code: "copy('${1:kolon}', '${2:Hedef}')\ncopy('${3:kolon2}', '${4:Hedef2}')",
}, },
{ {
label: 'Onay iste, vazgeçilirse geri al', label: 'ListForms.ScriptBuilder.Snippet.AskConfirm.Label',
group: 'Etkileşim', group: 'ListForms.ScriptBuilder.Snippet.Group.Interaction',
description: 'ask false dönerse alan eski değerine döner.', description: 'ListForms.ScriptBuilder.Snippet.AskConfirm.Description',
code: "if (!ask('${1:Emin misiniz?}')) return", code: "if (!ask('${1:Emin misiniz?}')) return",
}, },
{ {
label: 'API çağrısı ve hata yakalama', label: 'ListForms.ScriptBuilder.Snippet.ApiCall.Label',
group: 'Entegrasyon', group: 'ListForms.ScriptBuilder.Snippet.Group.Integration',
description: 'Çağrı başarısızsa uyarı gösterir.', description: 'ListForms.ScriptBuilder.Snippet.ApiCall.Description',
code: "try {\n set('${1:Hedef}', await api('${2:/api/app/musteri/{value}}', '${3:data.name}'))\n} catch (error) {\n notify('${4:Servise ulaşılamadı}')\n}", code: "try {\n set('${1:Hedef}', await api('${2:/api/app/musteri/{value}}', '${3:data.name}'))\n} catch (error) {\n notify('${4:Servise ulaşılamadı}')\n}",
}, },
{ {
label: 'Hesaplama ve yuvarlama', label: 'ListForms.ScriptBuilder.Snippet.Calc.Label',
group: 'Hesaplama', group: 'ListForms.ScriptBuilder.Snippet.Group.Calc',
description: 'İki alandan türetilmiş değer.', description: 'ListForms.ScriptBuilder.Snippet.Calc.Description',
code: "set('${1:Toplam}', round(num('${2:Miktar}') * num('${3:Fiyat}'), ${4:2}))", code: "set('${1:Toplam}', round(num('${2:Miktar}') * num('${3:Fiyat}'), ${4:2}))",
}, },
{ {
label: 'Alanı koşula göre kilitle', label: 'ListForms.ScriptBuilder.Snippet.LockField.Label',
group: 'Görünüm', group: 'ListForms.ScriptBuilder.Snippet.Group.View',
description: 'Koşul doğruyken salt okunur olur.', description: 'ListForms.ScriptBuilder.Snippet.LockField.Description',
code: "readOnly('${1:Alan}', str('${2:Durum}') === '${3:Kapali}')", code: "readOnly('${1:Alan}', str('${2:Durum}') === '${3:Kapali}')",
}, },
] ]
@ -139,9 +139,12 @@ export type FormScriptDialectContext = {
fields: string[] fields: string[]
/** Script'in bağlı olduğu alan; başlıkta gösterilir. */ /** Script'in bağlı olduğu alan; başlıkta gösterilir. */
currentField?: string currentField?: string
/** `useLocalization` hook'undan gelen çeviri fonksiyonu. */
translate: TranslateFn
} }
export const createFormScriptDialect = (context: FormScriptDialectContext): ScriptDialect => { export const createFormScriptDialect = (context: FormScriptDialectContext): ScriptDialect => {
const t = context.translate
const fields = Array.from(new Set(context.fields.filter(Boolean))).sort((left, right) => const fields = Array.from(new Set(context.fields.filter(Boolean))).sort((left, right) =>
left.localeCompare(right), left.localeCompare(right),
) )
@ -151,22 +154,51 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
return { return {
id: 'form', id: 'form',
title: 'Editor Script', title: 'Editor Script',
subject: context.currentField || 'alan seçilmemiş', subject: context.currentField || t('::ListForms.ScriptBuilder.NoFieldSelected'),
subjectHint: 'Bu alan değiştiğinde çalışacak script.', subjectHint: t('::ListForms.ScriptBuilder.SubjectHint'),
codeHint: codeHint: t('::ListForms.ScriptBuilder.CodeHint'),
'Tek satırlık yardımcılarla yaz; get/set/notify hazır. Ctrl+Space tamamlama listesini açar.',
groups: recipeGroups.map((group) => ({ groups: recipeGroups.map((group) => ({
key: group.key, key: group.key,
title: group.title, title: t('::' + group.title),
description: group.description, description: t('::' + group.description),
icon: groupIcons[group.key], icon: groupIcons[group.key],
})), })),
recipes: recipeViews, recipes: recipeViews.map((recipe) => ({
...recipe,
label: t('::' + recipe.label),
summary: t('::' + recipe.summary),
params: recipe.params.map((param) => ({
...param,
label: t('::' + param.label),
help: param.help ? t('::' + param.help) : undefined,
placeholder: param.placeholder?.startsWith('ListForms.')
? t('::' + param.placeholder)
: param.placeholder,
})),
})),
operators: DEFAULT_SCRIPT_OPERATORS, operators: DEFAULT_SCRIPT_OPERATORS,
conditionKinds: [{ value: FIELD_SOURCE, label: 'Form alanı', optionSource: FIELD_SOURCE }], conditionKinds: [
triggers: triggerLabels, {
apiReference, value: FIELD_SOURCE,
snippets, label: t('::ListForms.ScriptBuilder.FormField'),
optionSource: FIELD_SOURCE,
},
],
triggers: triggerLabels.map((trigger) => ({
...trigger,
label: t('::' + trigger.label),
help: t('::' + trigger.help),
})),
apiReference: apiReference.map((entry) => ({
...entry,
description: t('::' + entry.description),
})),
snippets: snippets.map((snippet) => ({
...snippet,
label: t('::' + snippet.label),
group: t('::' + snippet.group),
description: t('::' + snippet.description),
})),
getOptions: (optionSource) => (optionSource === FIELD_SOURCE ? fields : []), getOptions: (optionSource) => (optionSource === FIELD_SOURCE ? fields : []),
getRecipe: (recipeId) => recipeViews.find((recipe) => recipe.id === recipeId), getRecipe: (recipeId) => recipeViews.find((recipe) => recipe.id === recipeId),
createRule: (recipeId) => createRule(recipeId as RecipeId) as unknown as ScriptRule, createRule: (recipeId) => createRule(recipeId as RecipeId) as unknown as ScriptRule,
@ -184,18 +216,23 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] } return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
}, },
describeCondition: (condition) => describeCondition: (condition) =>
describeCondition(condition as unknown as FormScriptRule['condition']), describeCondition(condition as unknown as FormScriptRule['condition'], t),
describeRule: (rule) => { describeRule: (rule) => {
const recipe = recipesById.get(rule.recipe as RecipeId) const recipe = recipesById.get(rule.recipe as RecipeId)
if (!recipe) return '' if (!recipe) return ''
const action = recipe.describe(asFormRule(rule)) const action = recipe.describe(asFormRule(rule), t)
if (recipe.conditionIsArgument) return action if (recipe.conditionIsArgument) return action
const always = t('::ListForms.ScriptBuilder.Describe.Always')
const conditions = [rule.condition, ...(rule.conditions ?? [])] const conditions = [rule.condition, ...(rule.conditions ?? [])]
.map((condition) => describeCondition(condition as unknown as FormScriptRule['condition'])) .map((condition) =>
.filter((text) => text !== 'Her zaman') describeCondition(condition as unknown as FormScriptRule['condition'], t),
const prefix = conditions.length )
? conditions.join(rule.join === 'or' ? ' veya ' : ' ve ') .filter((text) => text !== always)
: 'Her zaman' const joiner =
rule.join === 'or'
? ` ${t('::ListForms.ScriptBuilder.Word.Or')} `
: ` ${t('::ListForms.ScriptBuilder.Word.And')} `
const prefix = conditions.length ? conditions.join(joiner) : always
return `${prefix}${action}` return `${prefix}${action}`
}, },
getTrigger: (rule) => effectiveTrigger(asFormRule(rule)), getTrigger: (rule) => effectiveTrigger(asFormRule(rule)),

View file

@ -34,13 +34,17 @@ export type RuleCondition = {
export type RuleTrigger = 'change' | 'open' | 'both' export type RuleTrigger = 'change' | 'open' | 'both'
export const triggerLabels: { value: RuleTrigger; label: string; help: string }[] = [ export const triggerLabels: { value: RuleTrigger; label: string; help: string }[] = [
{ value: 'change', label: 'Değiştiğinde', help: 'Yalnızca bu alanın değeri değişince çalışır.' }, {
value: 'change',
label: 'ListForms.ScriptBuilder.Change.Label',
help: 'ListForms.ScriptBuilder.Change.Help',
},
{ {
value: 'open', value: 'open',
label: 'Form açılırken', label: 'ListForms.ScriptBuilder.Open.Label',
help: 'Popup edit form açıldığında bir kez çalışır. Varsayılan değer üretmek için.', help: 'ListForms.ScriptBuilder.Open.Help',
}, },
{ value: 'both', label: 'Her ikisi', help: 'Hem açılışta hem değer değişiminde çalışır.' }, { value: 'both', label: 'Her ikisi', help: 'ListForms.ScriptBuilder.Both.Help' },
] ]
/** Ek koşullar nasıl birleşecek. */ /** Ek koşullar nasıl birleşecek. */
@ -93,7 +97,7 @@ export type Recipe = {
conditionIsArgument?: boolean conditionIsArgument?: boolean
params: RecipeParam[] params: RecipeParam[]
build: (rule: ScriptRule) => string build: (rule: ScriptRule) => string
describe: (rule: ScriptRule) => string describe: (rule: ScriptRule, t: TranslateFn) => string
example: string example: string
} }
@ -117,17 +121,31 @@ export type RecipeId =
| 'custom' | 'custom'
export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [ export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [
{ key: 'calc', title: 'Hesaplama', description: 'Alanlardan yeni bir değer üretir.' }, { key: 'calc', title: 'Hesaplama', description: 'ListForms.ScriptBuilder.Calc.Description' },
{ key: 'data', title: 'Veri', description: 'Alanlara değer yazar, kopyalar veya temizler.' }, { key: 'data', title: 'Veri', description: 'ListForms.ScriptBuilder.Data.Description' },
{ key: 'view', title: 'Görünüm', description: 'Alanların düzenlenebilirliğini yönetir.' }, {
{ key: 'interaction', title: 'Etkileşim', description: 'Kullanıcıya mesaj gösterir, sayfa açar.' }, key: 'view',
{ key: 'integration', title: 'Entegrasyon', description: 'API ve servis çağrıları, serbest kod.' }, title: 'ListForms.ScriptBuilder.View.Title',
description: 'ListForms.ScriptBuilder.View.Description',
},
{
key: 'interaction',
title: 'ListForms.ScriptBuilder.Interaction.Title',
description: 'ListForms.ScriptBuilder.Interaction.Description',
},
{
key: 'integration',
title: 'Entegrasyon',
description: 'ListForms.ScriptBuilder.Integration.Description',
},
] ]
/* ------------------------------------------------------------------ * /* ------------------------------------------------------------------ *
* Yardımcılar * Yardımcılar
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
export type TranslateFn = (key: string, params?: Record<string, string | number>) => string
const q = (input: string) => JSON.stringify(input ?? '') const q = (input: string) => JSON.stringify(input ?? '')
const label = (input: string, fallback: string) => input || fallback const label = (input: string, fallback: string) => input || fallback
@ -172,28 +190,31 @@ export const buildRuleConditionExpression = (rule: ScriptRule): string => {
return parts.map((part) => `(${part})`).join(rule.join === 'or' ? ' || ' : ' && ') return parts.map((part) => `(${part})`).join(rule.join === 'or' ? ' || ' : ' && ')
} }
export const describeCondition = (condition: RuleCondition): string => { export const describeCondition = (condition: RuleCondition, t: TranslateFn): string => {
if (condition.operator === 'always' || !condition.source) return 'Her zaman' if (condition.operator === 'always' || !condition.source) {
const source = label(condition.source, 'alan') return t('::ListForms.ScriptBuilder.Describe.Always')
}
const source = label(condition.source, t('::ListForms.ScriptBuilder.Word.Field'))
const value = label(condition.value, t('::ListForms.ScriptBuilder.Word.Value'))
switch (condition.operator) { switch (condition.operator) {
case 'empty': case 'empty':
return `${source} boşsa` return t('::ListForms.ScriptBuilder.Describe.Empty', { source })
case 'notEmpty': case 'notEmpty':
return `${source} doluysa` return t('::ListForms.ScriptBuilder.Describe.NotEmpty', { source })
case 'isTrue': case 'isTrue':
return `${source} işaretliyse` return t('::ListForms.ScriptBuilder.Describe.IsTrue', { source })
case 'isFalse': case 'isFalse':
return `${source} işaretli değilse` return t('::ListForms.ScriptBuilder.Describe.IsFalse', { source })
case 'contains': case 'contains':
return `${source} içinde "${label(condition.value, 'değer')}" geçiyorsa` return t('::ListForms.ScriptBuilder.Describe.Contains', { source, value })
case 'greaterThan': case 'greaterThan':
return `${source} > ${label(condition.value, 'değer')} ise` return t('::ListForms.ScriptBuilder.Describe.GreaterThan', { source, value })
case 'lessThan': case 'lessThan':
return `${source} < ${label(condition.value, 'değer')} ise` return t('::ListForms.ScriptBuilder.Describe.LessThan', { source, value })
case 'notEquals': case 'notEquals':
return `${source} ≠ "${label(condition.value, 'değer')}" ise` return t('::ListForms.ScriptBuilder.Describe.NotEquals', { source, value })
default: default:
return `${source} = "${label(condition.value, 'değer')}" ise` return t('::ListForms.ScriptBuilder.Describe.Equals', { source, value })
} }
} }
@ -206,81 +227,95 @@ const numberOrField = (raw: string) => (isNumericLiteral(raw) ? raw : `num(${q(r
export const recipes: Recipe[] = [ export const recipes: Recipe[] = [
{ {
id: 'multiply', id: 'multiply',
label: 'Çarpım (miktar × fiyat)', label: 'ListForms.ScriptBuilder.Multiply.Label',
group: 'calc', group: 'calc',
summary: 'İki alanı çarpar, yuvarlar ve hedefe yazar.', summary: 'ListForms.ScriptBuilder.Multiply.Summary',
example: "set('TotalAmount', round(num('Quantity') * num('UnitPrice'), 2))", example: "set('TotalAmount', round(num('Quantity') * num('UnitPrice'), 2))",
params: [ params: [
{ key: 'left', label: 'Birinci alan', type: 'field' }, { key: 'left', label: 'Birinci alan', type: 'field' },
{ key: 'right', label: 'İkinci alan', type: 'field' }, { key: 'right', label: 'ListForms.ScriptBuilder.Right.Label', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label', type: 'field' },
{ {
key: 'digits', key: 'digits',
label: 'Ondalık', label: 'ListForms.ScriptBuilder.Digits.Label',
type: 'number', type: 'number',
optional: true, optional: true,
placeholder: '2', placeholder: '2',
help: 'Boş bırakılırsa 2 kullanılır.', help: 'ListForms.ScriptBuilder.Digits.Help',
}, },
], ],
build: ({ params }) => build: ({ params }) =>
`set(${q(params.target)}, round(${numberOrField(params.left)} * ${numberOrField( `set(${q(params.target)}, round(${numberOrField(params.left)} * ${numberOrField(
params.right, params.right,
)}, ${params.digits || 2}))`, )}, ${params.digits || 2}))`,
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.left, 'alan')} × ${label(params.right, 'alan')} sonucunu ${label( t('::ListForms.ScriptBuilder.Describe.Multiply', {
params.target, left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
'hedef', right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
)} alanına yaz.`, target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
}, },
{ {
id: 'subtract', id: 'subtract',
label: 'Fark (brüt indirim)', label: 'ListForms.ScriptBuilder.Subtract.Label',
group: 'calc', group: 'calc',
summary: 'Birinci alandan ikinciyi çıkarır, yuvarlar ve hedefe yazar.', summary: 'ListForms.ScriptBuilder.Subtract.Summary',
example: "set('Net', round(num('Gross') - num('Discount'), 2))", example: "set('Net', round(num('Gross') - num('Discount'), 2))",
params: [ params: [
{ key: 'left', label: ıkarılan alan', type: 'field' }, { key: 'left', label: 'ListForms.ScriptBuilder.Left.Label', type: 'field' },
{ key: 'right', label: ıkan alan', type: 'field' }, { key: 'right', label: 'ListForms.ScriptBuilder.Right.Label2', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' }, {
key: 'digits',
label: 'ListForms.ScriptBuilder.Digits.Label',
type: 'number',
optional: true,
placeholder: '2',
},
], ],
build: ({ params }) => build: ({ params }) =>
`set(${q(params.target)}, round(${numberOrField(params.left)} - ${numberOrField( `set(${q(params.target)}, round(${numberOrField(params.left)} - ${numberOrField(
params.right, params.right,
)}, ${params.digits || 2}))`, )}, ${params.digits || 2}))`,
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.left, 'alan')} ${label(params.right, 'alan')} sonucunu ${label( t('::ListForms.ScriptBuilder.Describe.Subtract', {
params.target, left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
'hedef', right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
)} alanına yaz.`, target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
}, },
{ {
id: 'percent', id: 'percent',
label: 'Yüzde (KDV, iskonto)', label: 'ListForms.ScriptBuilder.Percent.Label',
group: 'calc', group: 'calc',
summary: 'Tutarın belirtilen yüzdesini hesaplar; oran alan ya da sabit olabilir.', summary: 'ListForms.ScriptBuilder.Percent.Summary',
example: "set('Kdv', round(num('Tutar') * num('KdvOrani') / 100, 2))", example: "set('Kdv', round(num('Tutar') * num('KdvOrani') / 100, 2))",
params: [ params: [
{ key: 'amount', label: 'Tutar alanı', type: 'field' }, { key: 'amount', label: 'ListForms.ScriptBuilder.Amount.Label', type: 'field' },
{ {
key: 'rate', key: 'rate',
label: 'Oran', label: 'Oran',
type: 'text', type: 'text',
placeholder: '20 veya KdvOrani', placeholder: '20 veya KdvOrani',
help: 'Sayı yazarsan sabit oran, alan adı yazarsan o alandan okunur.', help: 'ListForms.ScriptBuilder.Rate.Help',
},
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label3', type: 'field' },
{
key: 'digits',
label: 'ListForms.ScriptBuilder.Digits.Label2',
type: 'number',
optional: true,
placeholder: '2',
}, },
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
{ {
key: 'mode', key: 'mode',
label: 'Sonuç', label: 'ListForms.ScriptBuilder.Mode.Label',
type: 'select', type: 'select',
optional: true, optional: true,
choices: [ choices: [
{ value: '', label: 'Yalnızca yüzde tutarı' }, { value: '', label: 'ListForms.ScriptBuilder.X.Label' },
{ value: 'add', label: 'Tutar + yüzde' }, { value: 'add', label: 'ListForms.ScriptBuilder.Add.Label' },
{ value: 'subtract', label: 'Tutar yüzde' }, { value: 'subtract', label: 'ListForms.ScriptBuilder.Subtract.Label' },
], ],
}, },
], ],
@ -295,191 +330,215 @@ export const recipes: Recipe[] = [
: portion : portion
return `set(${q(params.target)}, round(${expression}, ${params.digits || 2}))` return `set(${q(params.target)}, round(${expression}, ${params.digits || 2}))`
}, },
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.amount, 'tutar')} üzerinden %${label(params.rate, 'oran')} hesabını ${label( t('::ListForms.ScriptBuilder.Describe.Percent', {
params.target, amount: label(params.amount, t('::ListForms.ScriptBuilder.Word.Amount')),
'hedef', rate: label(params.rate, t('::ListForms.ScriptBuilder.Word.Rate')),
)} alanına yaz.`, target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
}, },
{ {
id: 'today', id: 'today',
label: 'Bugünün tarihi', label: 'ListForms.ScriptBuilder.Today.Label',
group: 'calc', group: 'calc',
summary: 'Bugünü (veya kaç gün sonrasını) tarih alanına yazar.', summary: 'ListForms.ScriptBuilder.Today.Summary',
example: "set('Tarih', new Date().toISOString().slice(0, 10))", example: "set('Tarih', new Date().toISOString().slice(0, 10))",
params: [ params: [
{ key: 'target', label: 'Hedef alan', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{ {
key: 'offset', key: 'offset',
label: 'Gün ekle', label: 'ListForms.ScriptBuilder.Offset.Label',
type: 'number', type: 'number',
optional: true, optional: true,
placeholder: '0', placeholder: '0',
help: 'Negatif yazarsan geçmiş bir tarih üretir.', help: 'ListForms.ScriptBuilder.Offset.Help',
}, },
], ],
build: ({ params }) => { build: ({ params }) => {
const offset = Number(params.offset) || 0 const offset = Number(params.offset) || 0
const base = offset const base = offset ? `new Date(Date.now() + ${offset} * 86400000)` : 'new Date()'
? `new Date(Date.now() + ${offset} * 86400000)`
: 'new Date()'
return `set(${q(params.target)}, ${base}.toISOString().slice(0, 10))` return `set(${q(params.target)}, ${base}.toISOString().slice(0, 10))`
}, },
describe: ({ params }) => { describe: ({ params }, t) => {
const offset = Number(params.offset) || 0 const offset = Number(params.offset) || 0
const when = offset > 0 ? `${offset} gün sonrasını` : offset < 0 ? `${-offset} gün öncesini` : 'bugünü' const when =
return `${when} ${label(params.target, 'hedef')} alanına yaz.` offset > 0
? t('::ListForms.ScriptBuilder.Word.DaysLater', { count: offset })
: offset < 0
? t('::ListForms.ScriptBuilder.Word.DaysEarlier', { count: -offset })
: t('::ListForms.ScriptBuilder.Word.Today')
return t('::ListForms.ScriptBuilder.Describe.Today', {
when,
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
})
}, },
}, },
{ {
id: 'sum', id: 'sum',
label: 'Toplama', label: 'Toplama',
group: 'calc', group: 'calc',
summary: 'Seçilen alanları toplar ve hedefe yazar.', summary: 'ListForms.ScriptBuilder.Sum.Summary',
example: "set('Total', round(sum('Net', 'Tax'), 2))", example: "set('Total', round(sum('Net', 'Tax'), 2))",
params: [ params: [
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label4', type: 'field' },
{ key: 'sources', label: 'Toplanacak alanlar', type: 'fieldList' }, { key: 'sources', label: 'ListForms.ScriptBuilder.Sources.Label3', type: 'fieldList' },
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' }, {
key: 'digits',
label: 'ListForms.ScriptBuilder.Digits.Label3',
type: 'number',
optional: true,
placeholder: '2',
},
], ],
build: ({ params, fields }) => build: ({ params, fields }) =>
`set(${q(params.target)}, round(sum(${(fields ?? []).map(q).join(', ')}), ${ `set(${q(params.target)}, round(sum(${(fields ?? []).map(q).join(', ')}), ${
params.digits || 2 params.digits || 2
}))`, }))`,
describe: ({ params, fields }) => describe: ({ params, fields }, t) =>
`${(fields ?? []).join(' + ') || 'alanlar'} toplamını ${label( t('::ListForms.ScriptBuilder.Describe.Sum', {
params.target, sources: (fields ?? []).join(' + ') || t('::ListForms.ScriptBuilder.Word.Fields'),
'hedef', target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
)} alanına yaz.`, }),
}, },
{ {
id: 'formula', id: 'formula',
label: 'Serbest formül', label: 'ListForms.ScriptBuilder.Formula.Label',
group: 'calc', group: 'calc',
summary: 'JavaScript ifadesi yazarsın; num/str/get/selected kullanabilirsin.', summary: 'ListForms.ScriptBuilder.Formula.Summary',
example: "set('Net', num('Gross') - num('Discount'))", example: "set('Net', num('Gross') - num('Discount'))",
params: [ params: [
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label5', type: 'field' },
{ {
key: 'expression', key: 'expression',
label: 'İfade', label: 'ListForms.ScriptBuilder.Expression.Label',
type: 'text', type: 'text',
placeholder: "num('Gross') * 0.18", placeholder: "num('Gross') * 0.18",
help: "Kullanılabilir: num('X'), str('X'), get('X'), value, selected('Yol'), round(x, 2)", help: 'ListForms.ScriptBuilder.Expression.Help',
}, },
], ],
build: ({ params }) => `set(${q(params.target)}, ${params.expression || 'null'})`, build: ({ params }) => `set(${q(params.target)}, ${params.expression || 'null'})`,
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.expression, 'ifade')} sonucunu ${label(params.target, 'hedef')} alanına yaz.`, t('::ListForms.ScriptBuilder.Describe.Formula', {
expression: label(params.expression, t('::ListForms.ScriptBuilder.Word.Expression')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
}, },
{ {
id: 'days', id: 'days',
label: 'Gün farkı', label: 'ListForms.ScriptBuilder.Days.Label',
group: 'calc', group: 'calc',
summary: 'İki tarih arasındaki gün sayısını (bitiş dahil) yazar.', summary: 'ListForms.ScriptBuilder.Days.Summary',
example: "set('TotalDays', days('StartDate', 'EndDate'))", example: "set('TotalDays', days('StartDate', 'EndDate'))",
params: [ params: [
{ key: 'start', label: 'Başlangıç tarihi', type: 'field' }, { key: 'start', label: 'ListForms.ScriptBuilder.Start.Label', type: 'field' },
{ key: 'end', label: 'Bitiş tarihi', type: 'field' }, { key: 'end', label: 'ListForms.ScriptBuilder.End.Label', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label6', type: 'field' },
], ],
build: ({ params }) => build: ({ params }) => `set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`,
`set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`, describe: ({ params }, t) =>
describe: ({ params }) => t('::ListForms.ScriptBuilder.Describe.Days', {
`${label(params.start, 'başlangıç')}${label(params.end, 'bitiş')} gün farkını ${label( start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
params.target, end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
'hedef', target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
)} alanına yaz.`, }),
}, },
{ {
id: 'hours', id: 'hours',
label: 'Saat farkı', label: 'ListForms.ScriptBuilder.Hours.Label',
group: 'calc', group: 'calc',
summary: 'İki saat arasındaki farkı yazar; bitiş küçükse ertesi güne taşar.', summary: 'ListForms.ScriptBuilder.Hours.Summary',
example: "set('TotalHours', hours('StartTime', 'EndTime'))", example: "set('TotalHours', hours('StartTime', 'EndTime'))",
params: [ params: [
{ key: 'start', label: 'Başlangıç saati', type: 'field' }, { key: 'start', label: 'ListForms.ScriptBuilder.Start.Label2', type: 'field' },
{ key: 'end', label: 'Bitiş saati', type: 'field' }, { key: 'end', label: 'ListForms.ScriptBuilder.End.Label2', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label7', type: 'field' },
], ],
build: ({ params }) => build: ({ params }) => `set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`,
`set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`, describe: ({ params }, t) =>
describe: ({ params }) => t('::ListForms.ScriptBuilder.Describe.Hours', {
`${label(params.start, 'başlangıç')}${label(params.end, 'bitiş')} saat farkını ${label( start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
params.target, end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
'hedef', target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
)} alanına yaz.`, }),
}, },
{ {
id: 'copy', id: 'copy',
label: 'Seçili kayıttan kopyala', label: 'ListForms.ScriptBuilder.Copy.Label',
group: 'data', group: 'data',
summary: 'Lookup/GridBox ile seçilen kaydın bir kolonunu form alanına taşır.', summary: 'ListForms.ScriptBuilder.Copy.Summary',
example: "copy('unitPrice', 'UnitPrice')", example: "copy('unitPrice', 'UnitPrice')",
params: [ params: [
{ {
key: 'source', key: 'source',
label: 'Seçili kayıttaki kolon/yol', label: 'ListForms.ScriptBuilder.Source.Label',
type: 'text', type: 'text',
placeholder: 'unitPrice veya Customer.Name', placeholder: 'unitPrice veya Customer.Name',
}, },
{ key: 'target', label: 'Hedef alan', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
], ],
build: ({ params }) => `copy(${q(params.source)}, ${q(params.target)})`, build: ({ params }) => `copy(${q(params.source)}, ${q(params.target)})`,
describe: ({ params }) => describe: ({ params }, t) =>
`Seçili kayıttaki ${label(params.source, 'kolon')} değerini ${label( t('::ListForms.ScriptBuilder.Describe.Copy', {
params.target, source: label(params.source, t('::ListForms.ScriptBuilder.Word.Column')),
'hedef', target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
)} alanına kopyala.`, }),
}, },
{ {
id: 'setValue', id: 'setValue',
label: 'Değer yaz', label: 'ListForms.ScriptBuilder.SetValue.Label',
group: 'data', group: 'data',
summary: 'Sabit metin veya {Alan} / {value} / {selected.Yol} tokenları yazar.', summary: 'ListForms.ScriptBuilder.SetValue.Summary',
example: "set('Aciklama', tpl('{Musteri} - {value}'))", example: "set('Aciklama', tpl('{Musteri} - {value}'))",
params: [ params: [
{ key: 'target', label: 'Hedef alan', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{ {
key: 'text', key: 'text',
label: 'Değer', label: 'ListForms.ScriptBuilder.Text.Label',
type: 'text', type: 'text',
placeholder: '{Musteri} - {value}', placeholder: '{Musteri} - {value}',
help: 'Token kullanabilirsin: {AlanAdi}, {value}, {selected.Name}', help: 'ListForms.ScriptBuilder.Text.Help',
}, },
], ],
build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`, build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`,
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.target, 'hedef')} alanına "${label(params.text, 'değer')}" yaz.`, t('::ListForms.ScriptBuilder.Describe.SetValue', {
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
text: label(params.text, t('::ListForms.ScriptBuilder.Word.Value')),
}),
}, },
{ {
id: 'clear', id: 'clear',
label: 'Alanları temizle', label: 'ListForms.ScriptBuilder.Clear.Label',
group: 'data', group: 'data',
summary: 'Seçilen alanları null yapar.', summary: 'ListForms.ScriptBuilder.Clear.Summary',
example: "clear('Il', 'Ilce')", example: "clear('Il', 'Ilce')",
params: [{ key: 'sources', label: 'Temizlenecek alanlar', type: 'fieldList' }], params: [
{ key: 'sources', label: 'ListForms.ScriptBuilder.Sources.Label2', type: 'fieldList' },
],
build: ({ fields }) => `clear(${(fields ?? []).map(q).join(', ')})`, build: ({ fields }) => `clear(${(fields ?? []).map(q).join(', ')})`,
describe: ({ fields }) => `${(fields ?? []).join(', ') || 'alanları'} temizle.`, describe: ({ fields }, t) =>
t('::ListForms.ScriptBuilder.Describe.Clear', {
sources: (fields ?? []).join(', ') || t('::ListForms.ScriptBuilder.Word.Fields'),
}),
}, },
{ {
id: 'readOnly', id: 'readOnly',
label: 'Salt okunur yap', label: 'Salt okunur yap',
group: 'view', group: 'view',
summary: 'Koşul sağlandığında alanı kilitler, sağlanmadığında açar.', summary: 'ListForms.ScriptBuilder.ReadOnly.Summary',
conditionIsArgument: true, conditionIsArgument: true,
example: "readOnly('SatisSebebi', str('Durum') === 'Aktif')", example: "readOnly('SatisSebebi', str('Durum') === 'Aktif')",
params: [ params: [
{ key: 'target', label: 'Kilitlenecek alan', type: 'field' }, { key: 'target', label: 'Kilitlenecek alan', type: 'field' },
{ {
key: 'invert', key: 'invert',
label: 'Tersine çevir', label: 'ListForms.ScriptBuilder.Invert.Label',
type: 'select', type: 'select',
optional: true, optional: true,
help: 'Evet seçilirse koşul sağlandığında alan açılır.', help: 'ListForms.ScriptBuilder.Invert.Help',
choices: [ choices: [
{ value: '', label: 'Hayır' }, { value: '', label: 'ListForms.ScriptBuilder.X.Label2' },
{ value: 'true', label: 'Evet' }, { value: 'true', label: 'ListForms.ScriptBuilder.Yes' },
], ],
}, },
], ],
@ -488,39 +547,62 @@ export const recipes: Recipe[] = [
const flag = rule.params.invert === 'true' ? `!(${expression})` : expression const flag = rule.params.invert === 'true' ? `!(${expression})` : expression
return `readOnly(${q(rule.params.target)}, ${flag})` return `readOnly(${q(rule.params.target)}, ${flag})`
}, },
describe: ({ params, condition }) => describe: ({ params, condition }, t) =>
`${describeCondition(condition)} ${label(params.target, 'alan')} alanını ${ t('::ListForms.ScriptBuilder.Describe.ReadOnly', {
params.invert === 'true' ? 'düzenlenebilir' : 'salt okunur' condition: describeCondition(condition, t),
} yap.`, target: label(params.target, t('::ListForms.ScriptBuilder.Word.Field')),
mode: t(
params.invert === 'true'
? '::ListForms.ScriptBuilder.Word.Editable'
: '::ListForms.ScriptBuilder.Word.ReadOnly',
),
}),
}, },
{ {
id: 'notify', id: 'notify',
label: 'Uyarı göster', label: 'ListForms.ScriptBuilder.Notify.Label',
group: 'interaction', group: 'interaction',
summary: 'Kullanıcıya bilgi mesajı gösterir.', summary: 'ListForms.ScriptBuilder.Notify.Summary',
example: "notify('Limit aşıldı')", example: "notify('Limit aşıldı')",
params: [ params: [
{ key: 'message', label: 'Mesaj', type: 'text', placeholder: '{Musteri} limiti aşıldı' }, {
key: 'message',
label: 'ListForms.ScriptBuilder.Message.Label',
type: 'text',
placeholder: 'ListForms.ScriptBuilder.Notify.MessagePlaceholder',
},
], ],
build: ({ params }) => `notify(${q(params.message)})`, build: ({ params }) => `notify(${q(params.message)})`,
describe: ({ params }) => `"${label(params.message, 'mesaj')}" uyarısını göster.`, describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Notify', {
message: label(params.message, t('::ListForms.ScriptBuilder.Word.Message')),
}),
}, },
{ {
id: 'ask', id: 'ask',
label: 'Onay iste', label: 'ListForms.ScriptBuilder.Ask.Label',
group: 'interaction', group: 'interaction',
summary: 'Onay ister; kullanıcı vazgeçerse alan eski değerine döner.', summary: 'ListForms.ScriptBuilder.Ask.Summary',
example: "if (!ask('Emin misiniz?')) return", example: "if (!ask('Emin misiniz?')) return",
params: [{ key: 'message', label: 'Mesaj', type: 'text', placeholder: 'Emin misiniz?' }], params: [
{
key: 'message',
label: 'ListForms.ScriptBuilder.Message.Label',
type: 'text',
placeholder: 'ListForms.ScriptBuilder.Ask.MessagePlaceholder',
},
],
build: ({ params }) => `if (!ask(${q(params.message)})) return`, build: ({ params }) => `if (!ask(${q(params.message)})) return`,
describe: ({ params }) => describe: ({ params }, t) =>
`"${label(params.message, 'mesaj')}" onayını iste; vazgeçilirse değeri geri al.`, t('::ListForms.ScriptBuilder.Describe.Ask', {
message: label(params.message, t('::ListForms.ScriptBuilder.Word.Message')),
}),
}, },
{ {
id: 'openUrl', id: 'openUrl',
label: 'URL aç', label: 'ListForms.ScriptBuilder.OpenUrl.Label',
group: 'interaction', group: 'interaction',
summary: 'Yeni sekmede veya aynı sekmede adres açar.', summary: 'ListForms.ScriptBuilder.OpenUrl.Summary',
example: "openUrl('/report?id={Id}')", example: "openUrl('/report?id={Id}')",
params: [ params: [
{ key: 'url', label: 'URL', type: 'text', placeholder: '/report?id={Id}' }, { key: 'url', label: 'URL', type: 'text', placeholder: '/report?id={Id}' },
@ -539,16 +621,19 @@ export const recipes: Recipe[] = [
params.target && params.target !== '_blank' params.target && params.target !== '_blank'
? `openUrl(${q(params.url)}, ${q(params.target)})` ? `openUrl(${q(params.url)}, ${q(params.target)})`
: `openUrl(${q(params.url)})`, : `openUrl(${q(params.url)})`,
describe: ({ params }) => `${label(params.url, 'adres')} adresini aç.`, describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.OpenUrl', {
url: label(params.url, t('::ListForms.ScriptBuilder.Word.Address')),
}),
}, },
{ {
id: 'apiToField', id: 'apiToField',
label: 'API sonucunu alana yaz', label: 'API sonucunu alana yaz',
group: 'integration', group: 'integration',
summary: 'Adresi çağırır, JSON içinden okuduğu değeri alana yazar.', summary: 'ListForms.ScriptBuilder.ApiToField.Summary',
example: "set('Unvan', await api('/api/musteri/{value}', 'data.name'))", example: "set('Unvan', await api('/api/musteri/{value}', 'data.name'))",
params: [ params: [
{ key: 'target', label: 'Hedef alan', type: 'field' }, { key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{ key: 'url', label: 'API adresi', type: 'text', placeholder: '/api/musteri/{value}' }, { key: 'url', label: 'API adresi', type: 'text', placeholder: '/api/musteri/{value}' },
{ {
key: 'path', key: 'path',
@ -556,24 +641,24 @@ export const recipes: Recipe[] = [
type: 'text', type: 'text',
optional: true, optional: true,
placeholder: 'data.name', placeholder: 'data.name',
help: 'Boş bırakılırsa tüm JSON yazılır.', help: 'ListForms.ScriptBuilder.Path.Help',
}, },
], ],
build: ({ params }) => build: ({ params }) =>
`set(${q(params.target)}, await api(${q(params.url)}${ `set(${q(params.target)}, await api(${q(params.url)}${
params.path ? `, ${q(params.path)}` : '' params.path ? `, ${q(params.path)}` : ''
}))`, }))`,
describe: ({ params }) => describe: ({ params }, t) =>
`${label(params.url, 'api')} çağrısının sonucunu ${label( t('::ListForms.ScriptBuilder.Describe.ApiToField', {
params.target, url: label(params.url, 'api'),
'hedef', target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
)} alanına yaz.`, }),
}, },
{ {
id: 'custom', id: 'custom',
label: 'Serbest tek satır', label: 'ListForms.ScriptBuilder.Custom.Label',
group: 'integration', group: 'integration',
summary: 'Servis çağrısı veya kendi yazdığın tek satırlık kod.', summary: 'ListForms.ScriptBuilder.Custom.Summary',
example: 'UiEvalService.ApiGenerateBackgroundWorkers()', example: 'UiEvalService.ApiGenerateBackgroundWorkers()',
params: [ params: [
{ {
@ -584,7 +669,10 @@ export const recipes: Recipe[] = [
}, },
], ],
build: ({ params }) => (params.code || '').trim().replace(/;$/, ''), build: ({ params }) => (params.code || '').trim().replace(/;$/, ''),
describe: ({ params }) => label(params.code, 'kod') + ' çalıştır.', describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Custom', {
code: label(params.code, t('::ListForms.ScriptBuilder.Word.Code')),
}),
}, },
] ]
@ -741,25 +829,36 @@ export const createRule = (recipe: RecipeId): ScriptRule => ({
/** Cheatsheet olarak gösterilen runtime API özeti. */ /** Cheatsheet olarak gösterilen runtime API özeti. */
export const runtimeApiReference: { signature: string; description: string }[] = [ export const runtimeApiReference: { signature: string; description: string }[] = [
{ signature: 'value / field', description: 'Değişen alanın değeri ve adı.' }, { signature: 'value / field', description: 'ListForms.ScriptBuilder.Api.ValueField' },
{ signature: "get('Alan')", description: 'Form değerini okur (büyük/küçük harf duyarsız).' }, { signature: "get('Alan')", description: 'ListForms.ScriptBuilder.Api.Get' },
{ signature: "num('Alan') / str('Alan') / bool('Alan')", description: 'Tip dönüşümlü okuma.' }, {
{ signature: "dateOf('Alan')", description: 'Date nesnesi ya da null döner.' }, signature: "num('Alan') / str('Alan') / bool('Alan')",
{ signature: "set('Alan', deger)", description: 'Alana yazar. Nesne de verebilirsin.' }, description: 'ListForms.ScriptBuilder.Api.Cast',
{ signature: "set({ A: 1, B: 2 })", description: 'Birden fazla alanı tek çağrıda yazar.' }, },
{ signature: "clear('A', 'B')", description: 'Alanları null yapar.' }, { signature: "dateOf('Alan')", description: 'ListForms.ScriptBuilder.Api.DateOf' },
{ signature: "copy('kolon', 'Alan')", description: 'Seçili lookup kaydından kopyalar.' }, { signature: "set('Alan', deger)", description: 'ListForms.ScriptBuilder.Api.Set' },
{ signature: "selected('Yol') / snum() / sstr()", description: 'Seçili kaydı okur.' }, { signature: 'set({ A: 1, B: 2 })', description: 'ListForms.ScriptBuilder.Api.SetMany' },
{ signature: "readOnly('Alan', kosul)", description: 'Alanı kilitler veya açar.' }, { signature: "clear('A', 'B')", description: 'ListForms.ScriptBuilder.Api.Clear' },
{ signature: "round(x, 2) / sum('A','B')", description: 'Yuvarlama ve toplama.' }, { signature: "copy('kolon', 'Alan')", description: 'ListForms.ScriptBuilder.Api.Copy' },
{ signature: "days('Bas','Bit') / hours('Bas','Bit')", description: 'Tarih ve saat farkı.' }, {
{ signature: "tpl('{Alan} - {value}')", description: 'Token doldurur.' }, signature: "selected('Yol') / snum() / sstr()",
{ signature: "notify('mesaj') / ask('mesaj')", description: 'Uyarı ve onay. ask false ise geri alır.' }, description: 'ListForms.ScriptBuilder.Api.Selected',
{ signature: "openUrl('/adres')", description: 'Yeni sekmede açar.' }, },
{ signature: "await api('/api/x', 'data.name')", description: 'JSON çağrısı yapar.' }, { signature: "readOnly('Alan', kosul)", description: 'ListForms.ScriptBuilder.Api.ReadOnly' },
{ signature: "round(x, 2) / sum('A','B')", description: 'ListForms.ScriptBuilder.Api.Math' },
{
signature: "days('Bas','Bit') / hours('Bas','Bit')",
description: 'ListForms.ScriptBuilder.Api.DateDiff',
},
{ signature: "tpl('{Alan} - {value}')", description: 'ListForms.ScriptBuilder.Api.Tpl' },
{
signature: "notify('mesaj') / ask('mesaj')",
description: 'ListForms.ScriptBuilder.Api.NotifyAsk',
},
{ signature: "openUrl('/adres')", description: 'ListForms.ScriptBuilder.Api.OpenUrl' },
{ signature: "await api('/api/x', 'data.name')", description: 'ListForms.ScriptBuilder.Api.Api' },
{ {
signature: 'isReady', signature: 'isReady',
description: description: 'ListForms.ScriptBuilder.Api.IsReady',
'Form açılışında true, değer değişiminde false. Script açılışta da çalışsın istiyorsan bu ifadeyi kullan.',
}, },
] ]

View file

@ -618,7 +618,7 @@ const Wizard = () => {
} catch { } catch {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard file.'} {translate('::App.Listforms.WizardFileLoadError')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -789,7 +789,7 @@ const Wizard = () => {
const handleDeploy = async () => { const handleDeploy = async () => {
const formik = formikRef.current const formik = formikRef.current
if (!formik) throw new Error(translate('::App.Platform.Unknown') || 'Form bulunamadı') if (!formik) throw new Error(translate('::App.Platform.Unknown'))
const values = formik.values const values = formik.values
@ -861,11 +861,13 @@ const Wizard = () => {
return ( return (
<Container> <Container>
<PageTitle title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))} /> <PageTitle
title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))}
/>
{isLoadingEditData && ( {isLoadingEditData && (
<p className="text-xs text-gray-400 text-center py-4 animate-pulse"> <p className="text-xs text-gray-400 text-center py-4 animate-pulse">
{translate('::App.Loading') || 'Loading...'} {translate('::App.Loading')}
</p> </p>
)} )}
@ -879,32 +881,32 @@ const Wizard = () => {
> >
{({ touched, errors, values }) => { {({ touched, errors, values }) => {
const activeSteps = [ const activeSteps = [
{ id: 0, title: translate('::ListForms.Wizard.MenuInfo') || 'Menu Info' }, { id: 0, title: translate('::ListForms.Wizard.MenuInfo') },
{ {
id: 1, id: 1,
title: translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings', title: translate('::ListForms.Wizard.ListFormSettings'),
}, },
{ {
id: 2, id: 2,
title: translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields', title: translate('::ListForms.Wizard.ListFormFields'),
}, },
{ {
id: 3, id: 3,
title: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms', title: translate('::ListForms.ListFormEdit.SubForms'),
}, },
{ {
id: 4, id: 4,
title: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets', title: translate('::ListForms.ListFormEdit.TabWidgets'),
}, },
{ {
id: 5, id: 5,
title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow', title: translate('::ListForms.ListFormEdit.TabWorkflow'),
}, },
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({ ...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
id: step.id, id: step.id,
title: translate(step.translationKey) || step.fallbackTitle, title: translate(step.translationKey) || step.fallbackTitle,
})), })),
{ id: 10, title: translate('::App.Platform.Deploy') || 'Deploy' }, { id: 10, title: translate('::App.Platform.Deploy') },
] ]
// Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa // Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa
@ -931,7 +933,7 @@ const Wizard = () => {
<Steps.Item <Steps.Item
customIcon={<FaFolderOpen />} customIcon={<FaFolderOpen />}
status="pending" status="pending"
title={translate('::App.Listforms.WizardManager') || 'Wizard Manager'} title={translate('::App.Listforms.WizardManager')}
onStepChange={() => onStepChange={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager) navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager)
} }

View file

@ -57,7 +57,7 @@ const WizardFileManager = () => {
} catch { } catch {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard files.'} {translate('::App.Listforms.WizardFileLoadError')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -79,8 +79,7 @@ const WizardFileManager = () => {
toast.push( toast.push(
<Notification type="success" duration={3000}> <Notification type="success" duration={3000}>
<strong>{confirm.wizardName}</strong>{' '} <strong>{confirm.wizardName}</strong>{' '}
{translate('::App.Listforms.WizardFileDeleteSuccess') || {translate('::App.Listforms.WizardFileDeleteSuccess')}
'wizard file deleted successfully.'}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -88,8 +87,7 @@ const WizardFileManager = () => {
} catch (err: any) { } catch (err: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
{translate('::App.Listforms.WizardFileDeleteError') || 'Failed to delete wizard file.'}:{' '} {translate('::App.Listforms.WizardFileDeleteError')}: {err?.message ?? 'Unknown error'}
{err?.message ?? 'Unknown error'}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -110,7 +108,7 @@ const WizardFileManager = () => {
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
{MenuIcon} {MenuIcon}
<h4 className="text-sm font-medium truncate"> <h4 className="text-sm font-medium truncate">
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'} {translate('::App.Listforms.WizardManager')}
</h4> </h4>
</div> </div>
@ -120,7 +118,7 @@ const WizardFileManager = () => {
<Input <Input
size="sm" size="sm"
className="pl-6 w-36 sm:w-44" className="pl-6 w-36 sm:w-44"
placeholder={translate('::App.Platform.Search') || 'Search...'} placeholder={translate('::App.Platform.Search')}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
@ -128,7 +126,7 @@ const WizardFileManager = () => {
<Button <Button
size="sm" size="sm"
variant="default" variant="default"
title={translate('::App.Platform.Refresh') || 'Yenile'} title={translate('::App.Platform.Refresh')}
loading={loading} loading={loading}
onClick={loadFiles} onClick={loadFiles}
> >
@ -142,7 +140,7 @@ const WizardFileManager = () => {
className="flex items-center" className="flex items-center"
> >
<FaPlus className="mr-1" /> <FaPlus className="mr-1" />
<span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}</span> <span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord')}</span>
</Button> </Button>
</div> </div>
</div> </div>
@ -152,7 +150,7 @@ const WizardFileManager = () => {
<div className="text-center py-12"> <div className="text-center py-12">
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" /> <FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
<p className="text-sm text-gray-400 text-center py-4"> <p className="text-sm text-gray-400 text-center py-4">
{translate('::App.Listforms.WizardNoFiles') || 'No wizard files found.'} {translate('::App.Listforms.WizardNoFiles')}
</p> </p>
</div> </div>
)} )}
@ -188,10 +186,7 @@ const WizardFileManager = () => {
<div className="flex items-center gap-2 shrink-0 sm:ml-3"> <div className="flex items-center gap-2 shrink-0 sm:ml-3">
{!f.hasInsertedRecords && ( {!f.hasInsertedRecords && (
<span <span
title={ title={translate('::App.Listforms.WizardFileNoTrackedRecords')}
translate('::App.Listforms.WizardFileNoTrackedRecords') ||
'Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir.'
}
className="text-yellow-500 text-xs flex items-center gap-1" className="text-yellow-500 text-xs flex items-center gap-1"
> >
<FaExclamationTriangle /> <FaExclamationTriangle />
@ -202,7 +197,7 @@ const WizardFileManager = () => {
variant="plain" variant="plain"
className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20" className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
type="button" type="button"
title={translate('::App.Platform.Edit') || 'Edit'} title={translate('::App.Platform.Edit')}
onClick={() => onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, { navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName }, state: { editFileName: f.fileName },
@ -216,7 +211,7 @@ const WizardFileManager = () => {
variant="plain" variant="plain"
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20" className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
type="button" type="button"
title={translate('::Delete') || 'Delete'} title={translate('::Delete')}
loading={deletingFile === f.fileName} loading={deletingFile === f.fileName}
onClick={() => onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName }) setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
@ -256,7 +251,7 @@ const WizardFileManager = () => {
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>
<Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}> <Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}>
{translate('::Delete') || 'Yes, Delete'} {translate('::Delete')}
</Button> </Button>
</Dialog.Footer> </Dialog.Footer>
</Dialog> </Dialog>

View file

@ -372,7 +372,7 @@ function MenuTreeInline({
if (!node.id) return if (!node.id) return
if ( if (
!window.confirm( !window.confirm(
`"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm') || 'menüsünü silmek istediğinize emin misiniz?'}`, `"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm')}`,
) )
) )
return return
@ -422,7 +422,7 @@ function MenuTreeInline({
<div className="px-4 py-3 text-sm text-gray-400">Loading</div> <div className="px-4 py-3 text-sm text-gray-400">Loading</div>
) : enrichedNodes.length === 0 ? ( ) : enrichedNodes.length === 0 ? (
<div className="px-4 py-3 text-sm text-gray-400"> <div className="px-4 py-3 text-sm text-gray-400">
{translate('::ListForms.Wizard.Step1.NoMenusAvailable') || 'No menus available'} {translate('::ListForms.Wizard.Step1.NoMenusAvailable')}
</div> </div>
) : ( ) : (
enrichedNodes.map((node) => ( enrichedNodes.map((node) => (
@ -504,8 +504,7 @@ const WizardStep1 = ({
!values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'), !values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için // Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu. // burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
!(Number(values.menuOrder) >= 1) && !(Number(values.menuOrder) >= 1) && translate('::ListForms.Wizard.Step1.MenuOrder'),
(translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'),
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'), !values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'), !values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'), !values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
@ -521,8 +520,7 @@ const WizardStep1 = ({
asterisk={true} asterisk={true}
extra={ extra={
<span className="text-xs ml-2 text-gray-400"> <span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.WizardNameHint') || {translate('::ListForms.Wizard.Step1.WizardNameHint')}
'Used to generate ListForm Code and Menu Code'}
</span> </span>
} }
> >
@ -561,9 +559,7 @@ const WizardStep1 = ({
variant="solid" variant="solid"
icon={<FaPlus className="text-xs" />} icon={<FaPlus className="text-xs" />}
> >
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">{translate('::ListForms.Wizard.Add')}</span>
{translate('::ListForms.Wizard.Add') || 'Ekle'}
</span>
</Button> </Button>
{values.menuParentCode && ( {values.menuParentCode && (
<Button <Button
@ -578,7 +574,7 @@ const WizardStep1 = ({
icon={<FaTimes className="text-xs" />} icon={<FaTimes className="text-xs" />}
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600" className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
> >
{translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'} {translate('::ListForms.Wizard.ClearSelection')}
</Button> </Button>
)} )}
</div> </div>
@ -624,7 +620,7 @@ const WizardStep1 = ({
asterisk={true} asterisk={true}
extra={ extra={
<span className="text-xs ml-2 text-gray-400"> <span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.MenuCodeHint') || 'Auto-derived, editable'} {translate('::ListForms.Wizard.Step1.MenuCodeHint')}
</span> </span>
} }
> >
@ -640,14 +636,13 @@ const WizardStep1 = ({
{/* Menu Order */} {/* Menu Order */}
<FormItem <FormItem
label={translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'} label={translate('::ListForms.Wizard.Step1.MenuOrder')}
asterisk={true} asterisk={true}
invalid={!!(errors.menuOrder && touched.menuOrder)} invalid={!!(errors.menuOrder && touched.menuOrder)}
errorMessage={errors.menuOrder} errorMessage={errors.menuOrder}
extra={ extra={
<span className="text-xs ml-2 text-gray-400"> <span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.MenuOrderHint') || {translate('::ListForms.Wizard.Step1.MenuOrderHint')}
'Automatically assigned, editable'}
</span> </span>
} }
> >
@ -690,9 +685,7 @@ const WizardStep1 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextMenuEn" name="languageTextMenuEn"
placeholder={ placeholder={translate('::ListForms.Wizard.Step1.DisplayNameEnglish')}
translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -708,9 +701,7 @@ const WizardStep1 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextMenuTr" name="languageTextMenuTr"
placeholder={ placeholder={translate('::ListForms.Wizard.Step1.DisplayNameTurkish')}
translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -756,7 +747,7 @@ const WizardStep1 = ({
<div className="flex-1 flex items-center gap-2"> <div className="flex-1 flex items-center gap-2">
{!step1CanGo && ( {!step1CanGo && (
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium"> <span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
{translate('::ListForms.Wizard.Required') || 'Zorunlu'}: {step1Missing.join(', ')} {translate('::ListForms.Wizard.Required')}: {step1Missing.join(', ')}
</span> </span>
)} )}
</div> </div>
@ -767,7 +758,7 @@ const WizardStep1 = ({
disabled={!step1CanGo} disabled={!step1CanGo}
onClick={onNext} onClick={onNext}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -188,10 +188,7 @@ const WizardStep11 = ({
const [isDone, setIsDone] = useState(false) const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
const steps = useMemo( const steps = useMemo(() => buildLogSteps(values, groups, translate), [values, groups, translate])
() => buildLogSteps(values, groups, translate),
[values, groups, translate],
)
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@ -344,10 +341,8 @@ const WizardStep11 = ({
</div> </div>
<Section <Section
title={ title={translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout')}
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout' badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm')}`}
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
> >
<div className="mb-3 grid grid-cols-3 gap-2"> <div className="mb-3 grid grid-cols-3 gap-2">
{[ {[
@ -357,7 +352,7 @@ const WizardStep11 = ({
className: 'text-indigo-600 dark:text-indigo-400', className: 'text-indigo-600 dark:text-indigo-400',
}, },
{ {
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form', label: translate('::ListForms.Wizard.Step4.EditingForm'),
value: editingFormFields.length, value: editingFormFields.length,
className: 'text-emerald-600 dark:text-emerald-400', className: 'text-emerald-600 dark:text-emerald-400',
}, },
@ -401,7 +396,7 @@ const WizardStep11 = ({
<div className="divide-y divide-gray-100 dark:divide-gray-800"> <div className="divide-y divide-gray-100 dark:divide-gray-800">
{g.items.length === 0 ? ( {g.items.length === 0 ? (
<span className="block px-3 py-2 text-xs text-gray-300 italic"> <span className="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'} {translate('::ListForms.Wizard.Step4.NoFields')}
</span> </span>
) : ( ) : (
g.items.map((item) => { g.items.map((item) => {
@ -425,11 +420,11 @@ const WizardStep11 = ({
</div> </div>
<div className="flex flex-wrap justify-end gap-1"> <div className="flex flex-wrap justify-end gap-1">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300"> <span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'} {translate('::ListForms.Wizard.Step4.SelectedColumns')}
</span> </span>
{isPopupField && ( {isPopupField && (
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300"> <span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'} {translate('::ListForms.Wizard.Step4.EditingForm')}
</span> </span>
)} )}
{isKeyField && ( {isKeyField && (
@ -455,7 +450,7 @@ const WizardStep11 = ({
{ungroupedSelectedColumns.length > 0 && ( {ungroupedSelectedColumns.length > 0 && (
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2"> <div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300"> <div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'} {translate('::ListForms.Wizard.Step4.UngroupedColumns')}
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => { {ungroupedSelectedColumns.map((col) => {
@ -481,10 +476,7 @@ const WizardStep11 = ({
</Section> </Section>
{subForms.length > 0 && ( {subForms.length > 0 && (
<Section <Section title={translate('::ListForms.ListFormEdit.SubForms')} badge={subForms.length}>
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
badge={subForms.length}
>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{subForms.map((subForm, index) => ( {subForms.map((subForm, index) => (
<div <div
@ -517,10 +509,7 @@ const WizardStep11 = ({
)} )}
{widgets.length > 0 && ( {widgets.length > 0 && (
<Section <Section title={translate('::ListForms.ListFormEdit.TabWidgets')} badge={widgets.length}>
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
badge={widgets.length}
>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{widgets.map((widget, index) => ( {widgets.map((widget, index) => (
<div <div
@ -545,7 +534,7 @@ const WizardStep11 = ({
{(hasWorkflowFields || workflowItems.length > 0) && ( {(hasWorkflowFields || workflowItems.length > 0) && (
<Section <Section
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'} title={translate('::ListForms.ListFormEdit.TabWorkflow')}
badge={workflowItems.length} badge={workflowItems.length}
> >
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
@ -594,7 +583,7 @@ const WizardStep11 = ({
)} )}
{values.todo && ( {values.todo && (
<Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}> <Section title={translate('::ListForms.ListFormEdit.TabTodo')}>
<div className="grid grid-cols-2 gap-x-4"> <div className="grid grid-cols-2 gap-x-4">
<Row <Row
label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')} label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')}
@ -657,7 +646,7 @@ const WizardStep11 = ({
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length }, { label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields }, { label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{ {
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form', label: translate('::ListForms.Wizard.Step4.EditingForm'),
value: editingFormFields.length, value: editingFormFields.length,
}, },
{ {
@ -665,19 +654,19 @@ const WizardStep11 = ({
value: selectedColumns.size, value: selectedColumns.size,
}, },
{ {
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms', label: translate('::ListForms.ListFormEdit.SubForms'),
value: subForms.length, value: subForms.length,
}, },
{ {
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets', label: translate('::ListForms.ListFormEdit.TabWidgets'),
value: widgets.length, value: widgets.length,
}, },
{ {
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow', label: translate('::ListForms.ListFormEdit.TabWorkflow'),
value: workflowItems.length, value: workflowItems.length,
}, },
{ {
label: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo', label: translate('::ListForms.ListFormEdit.TabTodo'),
value: values.todo ? 1 : 0, value: values.todo ? 1 : 0,
}, },
].map((s) => ( ].map((s) => (
@ -698,7 +687,7 @@ const WizardStep11 = ({
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0"> <div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2"> <span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
<FaRocket className="text-indigo-400 text-xs" /> <FaRocket className="text-indigo-400 text-xs" />
{translate('::ListForms.Wizard.Step4.DeployLog') || 'Deploy Log'} {translate('::ListForms.Wizard.Step4.DeployLog')}
</span> </span>
{isDone && ( {isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1"> <span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
@ -771,7 +760,7 @@ const WizardStep11 = ({
disabled={isDeploying} disabled={isDeploying}
onClick={onBack} onClick={onBack}
> >
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end"> <div className="flex-1 flex items-center justify-end">
<Button <Button

View file

@ -124,8 +124,7 @@ const WizardStep2 = ({
!values.languageTextDescTr && translate('::ListForms.Wizard.Step2.DescriptionTextTurkish'), !values.languageTextDescTr && translate('::ListForms.Wizard.Step2.DescriptionTextTurkish'),
!values.languageTextTitleEn && translate('::ListForms.Wizard.Step2.TitleTextEnglish'), !values.languageTextTitleEn && translate('::ListForms.Wizard.Step2.TitleTextEnglish'),
!values.languageTextTitleTr && translate('::ListForms.Wizard.Step2.TitleTextTurkish'), !values.languageTextTitleTr && translate('::ListForms.Wizard.Step2.TitleTextTurkish'),
selectedColumns.size === 0 && selectedColumns.size === 0 && translate('::ListForms.Wizard.Step2.ColumnSelection'),
(translate('::ListForms.Wizard.Step2.ColumnSelection') || 'Sütun seçimi'),
].filter(Boolean) as string[] ].filter(Boolean) as string[]
const step2CanGo = step2Missing.length === 0 const step2CanGo = step2Missing.length === 0
@ -140,8 +139,7 @@ const WizardStep2 = ({
asterisk={true} asterisk={true}
extra={ extra={
<span className="text-xs ml-2 text-gray-400"> <span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step2.ListFormCodeHint') || {translate('::ListForms.Wizard.Step2.ListFormCodeHint')}
'Auto-derived from Wizard Name, editable'}
</span> </span>
} }
> >
@ -230,7 +228,7 @@ const WizardStep2 = ({
const grouped = dbObjects const grouped = dbObjects
? [ ? [
{ {
label: translate('::ListForms.Wizard.Step2.Tables') || 'Tables', label: translate('::ListForms.Wizard.Step2.Tables'),
options: dbObjects.tables.map((t) => ({ options: dbObjects.tables.map((t) => ({
label: t.tableName, label: t.tableName,
value: t.tableName, value: t.tableName,
@ -240,9 +238,7 @@ const WizardStep2 = ({
})), })),
}, },
{ {
label: label: translate('::ListForms.Wizard.Step2.StoredProcedures'),
translate('::ListForms.Wizard.Step2.StoredProcedures') ||
'Stored Procedures',
options: dbObjects.storedProcedures.map((p) => ({ options: dbObjects.storedProcedures.map((p) => ({
label: p.objectName, label: p.objectName,
value: p.objectName, value: p.objectName,
@ -252,7 +248,7 @@ const WizardStep2 = ({
})), })),
}, },
{ {
label: translate('::App.Platform.Views') || 'Views', label: translate('::App.Platform.Views'),
options: dbObjects.views.map((v) => ({ options: dbObjects.views.map((v) => ({
label: v.objectName, label: v.objectName,
value: v.objectName, value: v.objectName,
@ -262,7 +258,7 @@ const WizardStep2 = ({
})), })),
}, },
{ {
label: translate('::ListForms.Wizard.Step2.Functions') || 'Functions', label: translate('::ListForms.Wizard.Step2.Functions'),
options: dbObjects.functions.map((f) => ({ options: dbObjects.functions.map((f) => ({
label: f.objectName, label: f.objectName,
value: f.objectName, value: f.objectName,
@ -285,8 +281,7 @@ const WizardStep2 = ({
placeholder={ placeholder={
isLoadingDbObjects isLoadingDbObjects
? translate('::App.Loading') ? translate('::App.Loading')
: translate('::ListForms.Wizard.Step2.SelectCommand') || : translate('::ListForms.Wizard.Step2.SelectCommand')
'Tablo/View/SP seç veya SQL yaz…'
} }
value={field.value ? { label: field.value, value: field.value } : null} value={field.value ? { label: field.value, value: field.value } : null}
onChange={(option: any) => { onChange={(option: any) => {
@ -369,8 +364,8 @@ const WizardStep2 = ({
isLoading={isLoadingColumns} isLoading={isLoadingColumns}
placeholder={ placeholder={
isLoadingColumns isLoadingColumns
? translate('::ListForms.Wizard.Step2.ColumnsLoading') || 'Sütunlar yükleniyor…' ? translate('::ListForms.Wizard.Step2.ColumnsLoading')
: translate('::ListForms.Wizard.Step2.SelectKeyColumn') || 'Key sütunu seç…' : translate('::ListForms.Wizard.Step2.SelectKeyColumn')
} }
options={selectCommandColumns.map((c) => ({ options={selectCommandColumns.map((c) => ({
label: `${c.columnName} (${c.dataType})`, label: `${c.columnName} (${c.dataType})`,
@ -639,9 +634,7 @@ const WizardStep2 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextTitleEn" name="languageTextTitleEn"
placeholder={ placeholder={translate('::ListForms.Wizard.Step2.TitleTextEnglish')}
translate('::ListForms.Wizard.Step2.TitleTextEnglish') || 'English Title Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -656,9 +649,7 @@ const WizardStep2 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextTitleTr" name="languageTextTitleTr"
placeholder={ placeholder={translate('::ListForms.Wizard.Step2.TitleTextTurkish')}
translate('::ListForms.Wizard.Step2.TitleTextTurkish') || 'Turkish Title Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -673,10 +664,7 @@ const WizardStep2 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextDescEn" name="languageTextDescEn"
placeholder={ placeholder={translate('::ListForms.Wizard.Step2.DescriptionTextEnglish')}
translate('::ListForms.Wizard.Step2.DescriptionTextEnglish') ||
'English Description Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -691,10 +679,7 @@ const WizardStep2 = ({
type="text" type="text"
autoComplete="off" autoComplete="off"
name="languageTextDescTr" name="languageTextDescTr"
placeholder={ placeholder={translate('::ListForms.Wizard.Step2.DescriptionTextTurkish')}
translate('::ListForms.Wizard.Step2.DescriptionTextTurkish') ||
'Turkish Description Text'
}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
@ -702,7 +687,7 @@ const WizardStep2 = ({
{/* Column Selection Panel */} {/* Column Selection Panel */}
<FormItem <FormItem
label={translate('::ListForms.Wizard.Step2.Columns') || 'Sütunlar'} label={translate('::ListForms.Wizard.Step2.Columns')}
extra={ extra={
selectCommandColumns.length > 0 ? ( selectCommandColumns.length > 0 ? (
<div className="flex items-center gap-2 ml-3"> <div className="flex items-center gap-2 ml-3">
@ -712,7 +697,7 @@ const WizardStep2 = ({
onClick={() => onToggleAllColumns(true)} onClick={() => onToggleAllColumns(true)}
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600" className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
> >
{translate('::ListForms.Wizard.Step2.SelectAll') || 'Tümünü Seç'} {translate('::ListForms.Wizard.Step2.SelectAll')}
</Button> </Button>
<Button <Button
variant="default" variant="default"
@ -720,7 +705,7 @@ const WizardStep2 = ({
onClick={() => onToggleAllColumns(false)} onClick={() => onToggleAllColumns(false)}
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400" className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
> >
{translate('::ListForms.Wizard.Step2.ClearAll') || 'Tümünü Kaldır'} {translate('::ListForms.Wizard.Step2.ClearAll')}
</Button> </Button>
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
{selectedColumns.size}/{selectCommandColumns.length}{' '} {selectedColumns.size}/{selectCommandColumns.length}{' '}
@ -733,12 +718,11 @@ const WizardStep2 = ({
<div className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 overflow-hidden"> <div className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 overflow-hidden">
{isLoadingColumns ? ( {isLoadingColumns ? (
<div className="px-4 py-3 text-sm text-gray-400"> <div className="px-4 py-3 text-sm text-gray-400">
{translate('::ListForms.Wizard.Step2.ColumnsLoading') || 'Sütunlar yükleniyor…'} {translate('::ListForms.Wizard.Step2.ColumnsLoading')}
</div> </div>
) : selectCommandColumns.length === 0 ? ( ) : selectCommandColumns.length === 0 ? (
<div className="px-4 py-3 text-sm text-gray-400"> <div className="px-4 py-3 text-sm text-gray-400">
{translate('::ListForms.Wizard.Step2.ColumnsWillAppear') || {translate('::ListForms.Wizard.Step2.ColumnsWillAppear')}
'Select Command seçilince sütunlar burada görünecek'}
</div> </div>
) : ( ) : (
<div className="h-40 overflow-y-auto py-1"> <div className="h-40 overflow-y-auto py-1">
@ -775,7 +759,7 @@ const WizardStep2 = ({
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="flex items-center gap-3 w-full"> <div className="flex items-center gap-3 w-full">
<Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
{!step2CanGo && ( {!step2CanGo && (
@ -790,7 +774,7 @@ const WizardStep2 = ({
disabled={!step2CanGo} disabled={!step2CanGo}
onClick={onNext} onClick={onNext}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -387,7 +387,7 @@ function SortableItem({
icon={<FaPlus className="text-[8px]" />} icon={<FaPlus className="text-[8px]" />}
className="!h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40 shrink-0" className="!h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40 shrink-0"
> >
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'} {translate('::ListForms.Wizard.Step3.GenerateFromTable')}
</Button> </Button>
</div> </div>
<textarea <textarea
@ -423,7 +423,7 @@ function SortableItem({
)} )}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200"> <span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{pickerStep === 'table' {pickerStep === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç' ? translate('::ListForms.Wizard.Step3.SelectTable')
: (pickerTable?.tableName ?? '')} : (pickerTable?.tableName ?? '')}
</span> </span>
</div> </div>
@ -445,15 +445,14 @@ function SortableItem({
autoFocus autoFocus
value={tableSearch} value={tableSearch}
onChange={(e) => setTableSearch(e.target.value)} onChange={(e) => setTableSearch(e.target.value)}
placeholder={translate('::Search') || 'Ara...'} placeholder={translate('::Search')}
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
/> />
</div> </div>
<div className="overflow-y-auto flex-1 p-2"> <div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? ( {!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6"> <div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') || {translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
'Tablo bulunamadı'}
</div> </div>
) : ( ) : (
dbObjects.tables dbObjects.tables
@ -506,14 +505,14 @@ function SortableItem({
<> <>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu {translate('::ListForms.ListFormFieldEdit.KeyColumn')}
</label> </label>
<select <select
value={pickerKeyCol} value={pickerKeyCol}
onChange={(e) => setPickerKeyCol(e.target.value)} onChange={(e) => setPickerKeyCol(e.target.value)}
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
> >
<option value="">-- Seçiniz --</option> <option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => ( {pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}> <option key={c.columnName} value={c.columnName}>
{c.columnName} {c.columnName}
@ -523,14 +522,14 @@ function SortableItem({
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu {translate('::ListForms.ListFormFieldEdit.NameColumn')}
</label> </label>
<select <select
value={pickerNameCol} value={pickerNameCol}
onChange={(e) => setPickerNameCol(e.target.value)} onChange={(e) => setPickerNameCol(e.target.value)}
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
> >
<option value="">-- Seçiniz --</option> <option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => ( {pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}> <option key={c.columnName} value={c.columnName}>
{c.columnName} {c.columnName}
@ -565,7 +564,7 @@ function SortableItem({
variant="solid" variant="solid"
className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40" className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40"
> >
Tamam {translate('::OK')}
</Button> </Button>
</> </>
)} )}
@ -659,7 +658,7 @@ function SortableItem({
</div> </div>
<label <label
className="flex items-center gap-1 cursor-pointer ml-auto" className="flex items-center gap-1 cursor-pointer ml-auto"
title={translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'} title={translate('::ListForms.Wizard.Step3.IncludeInEditingForm')}
> >
<input <input
type="checkbox" type="checkbox"
@ -668,7 +667,7 @@ function SortableItem({
className="w-3 h-3 accent-indigo-500" className="w-3 h-3 accent-indigo-500"
/> />
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'} {translate('::ListForms.Wizard.Step3.IncludeInEditingForm')}
</span> </span>
</label> </label>
<label className="flex items-center gap-1 cursor-pointer ml-auto" title="Required"> <label className="flex items-center gap-1 cursor-pointer ml-auto" title="Required">
@ -679,7 +678,7 @@ function SortableItem({
className="w-3 h-3 accent-red-500" className="w-3 h-3 accent-red-500"
/> />
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.Required') || 'Required'} {translate('::ListForms.Wizard.Step3.Required')}
</span> </span>
</label> </label>
</div> </div>
@ -741,7 +740,7 @@ function GroupCard({
{/* ColCount */} {/* ColCount */}
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'} {translate('::ListForms.Wizard.Step3.Cols')}
</span> </span>
{[1, 2, 3].map((n) => ( {[1, 2, 3].map((n) => (
<Button <Button
@ -769,12 +768,9 @@ function GroupCard({
shape="none" shape="none"
icon={<FaArrowRight className="text-[9px]" />} icon={<FaArrowRight className="text-[9px]" />}
className="!h-6 !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-2 text-[11px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-900/40 shrink-0" className="!h-6 !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-2 text-[11px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-900/40 shrink-0"
title={ title={translate('::ListForms.Wizard.Step3.AddAllToGroupTitle')}
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
'Tüm mevcut sütunları bu gruba ekle'
}
> >
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'} {translate('::ListForms.Wizard.Step3.AddAll')}
</Button> </Button>
)} )}
<Button <Button
@ -784,7 +780,7 @@ function GroupCard({
shape="circle" shape="circle"
icon={<FaTrash className="text-xs" />} icon={<FaTrash className="text-xs" />}
className="!h-7 !w-7 !px-0 text-gray-300 transition-colors hover:!bg-transparent hover:text-red-500" className="!h-7 !w-7 !px-0 text-gray-300 transition-colors hover:!bg-transparent hover:text-red-500"
title={translate('::ListForms.Wizard.Step3.DeleteGroup') || 'Delete group'} title={translate('::ListForms.Wizard.Step3.DeleteGroup')}
/> />
</div> </div>
@ -800,7 +796,7 @@ function GroupCard({
> >
{group.items.length === 0 && !isOver && ( {group.items.length === 0 && !isOver && (
<div className="flex items-center justify-center h-12 text-xs text-gray-300 dark:text-gray-600 select-none"> <div className="flex items-center justify-center h-12 text-xs text-gray-300 dark:text-gray-600 select-none">
{translate('::ListForms.Wizard.Step3.DragColumnsHere') || 'Sütunları buraya sürükleyin'} {translate('::ListForms.Wizard.Step3.DragColumnsHere')}
</div> </div>
)} )}
<SortableContext items={itemIds} strategy={rectSortingStrategy}> <SortableContext items={itemIds} strategy={rectSortingStrategy}>
@ -1085,10 +1081,9 @@ const WizardStep3 = ({
const hasEmptyGroup = groups.some((g) => g.items.length === 0) const hasEmptyGroup = groups.some((g) => g.items.length === 0)
const canProceed = !hasNoGroups && !hasEmptyGroup const canProceed = !hasNoGroups && !hasEmptyGroup
const validationMsg = hasNoGroups const validationMsg = hasNoGroups
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.' ? translate('::ListForms.Wizard.Step3.AtLeastOneGroup')
: hasEmptyGroup : hasEmptyGroup
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn') || ? translate('::ListForms.Wizard.Step3.AtLeastOneColumn')
'Her gruba en az bir sütun eklemelisiniz.'
: '' : ''
return ( return (
@ -1114,8 +1109,7 @@ const WizardStep3 = ({
<div className="flex flex-row flex-wrap lg:flex-col gap-1.5 max-h-40 lg:max-h-[calc(100vh-280px)] overflow-y-auto pr-1"> <div className="flex flex-row flex-wrap lg:flex-col gap-1.5 max-h-40 lg:max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
{availableColumns.length === 0 ? ( {availableColumns.length === 0 ? (
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full"> <div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full">
{translate('::ListForms.Wizard.Step3.AllColumnsAdded') || {translate('::ListForms.Wizard.Step3.AllColumnsAdded')}
'Tüm sütunlar gruplara eklendi'}
</div> </div>
) : ( ) : (
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />) availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
@ -1128,8 +1122,7 @@ const WizardStep3 = ({
<div className="flex-1 flex flex-col gap-3"> <div className="flex-1 flex flex-col gap-3">
{groups.length === 0 && ( {groups.length === 0 && (
<div className="rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 flex items-center justify-center h-36 text-sm text-gray-300 dark:text-gray-600 select-none"> <div className="rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 flex items-center justify-center h-36 text-sm text-gray-300 dark:text-gray-600 select-none">
{translate('::ListForms.Wizard.Step3.NoGroupsYet') || {translate('::ListForms.Wizard.Step3.NoGroupsYet')}
'Henüz grup yok — aşağıdan grup ekleyin'}
</div> </div>
)} )}
@ -1160,7 +1153,7 @@ const WizardStep3 = ({
icon={<FaPlus className="text-xs" />} icon={<FaPlus className="text-xs" />}
className="w-full !h-auto !items-center !justify-center gap-2 !rounded-xl border-2 border-dashed border-gray-200 !py-2.5 text-sm text-gray-400 transition-colors hover:!bg-transparent hover:border-indigo-400 hover:text-indigo-500 dark:border-gray-700 dark:text-gray-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400" className="w-full !h-auto !items-center !justify-center gap-2 !rounded-xl border-2 border-dashed border-gray-200 !py-2.5 text-sm text-gray-400 transition-colors hover:!bg-transparent hover:border-indigo-400 hover:text-indigo-500 dark:border-gray-700 dark:text-gray-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400"
> >
{translate('::ListForms.Wizard.Step3.AddGroup') || 'Grup Ekle'} {translate('::ListForms.Wizard.Step3.AddGroup')}
</Button> </Button>
</div> </div>
</div> </div>
@ -1239,7 +1232,7 @@ const WizardStep3 = ({
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full"> <div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
@ -1248,7 +1241,7 @@ const WizardStep3 = ({
icon={<FaCode />} icon={<FaCode />}
onClick={() => setIsHelperOpen(true)} onClick={() => setIsHelperOpen(true)}
> >
{translate('::Helper Codes') || 'Helper Codes'} {translate('::Helper Codes')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
{!canProceed && ( {!canProceed && (
@ -1264,7 +1257,7 @@ const WizardStep3 = ({
disabled={!canProceed} disabled={!canProceed}
onClick={onNext} onClick={onNext}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -219,7 +219,7 @@ function WizardStep4({
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full"> <div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
<Button <Button
@ -229,7 +229,7 @@ function WizardStep4({
icon={<FaArrowRight />} icon={<FaArrowRight />}
onClick={onNext} onClick={onNext}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -143,7 +143,7 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full"> <div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
<Button <Button
@ -153,7 +153,7 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
icon={<FaArrowRight />} icon={<FaArrowRight />}
onClick={onNext} onClick={onNext}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -223,7 +223,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Start', listFormCode)), ...normalizeCriteria(emptyCriteria('Start', listFormCode)),
id: startId, id: startId,
nodeId: startId, nodeId: startId,
title: 'İş Akışı Başlat1', title: translate('::ListForms.Workflow.Demo.Start'),
nextOnStart: approval1Id, nextOnStart: approval1Id,
positionX: 34, positionX: 34,
positionY: 28, positionY: 28,
@ -232,7 +232,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)), ...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approval1Id, id: approval1Id,
nodeId: approval1Id, nodeId: approval1Id,
title: 'Onay1', title: translate('::ListForms.Workflow.Demo.Approval1'),
nextOnApprove: approval2Id, nextOnApprove: approval2Id,
nextOnReject: informId, nextOnReject: informId,
positionX: 323, positionX: 323,
@ -242,7 +242,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)), ...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approval2Id, id: approval2Id,
nodeId: approval2Id, nodeId: approval2Id,
title: 'Onay2', title: translate('::ListForms.Workflow.Demo.Approval2'),
nextOnApprove: informId, nextOnApprove: informId,
nextOnReject: informId, nextOnReject: informId,
positionX: 586, positionX: 586,
@ -252,7 +252,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Inform', listFormCode)), ...normalizeCriteria(emptyCriteria('Inform', listFormCode)),
id: informId, id: informId,
nodeId: informId, nodeId: informId,
title: 'Bilgilendirme1', title: translate('::ListForms.Workflow.Demo.Inform'),
nextOnStart: endId, nextOnStart: endId,
positionX: 458, positionX: 458,
positionY: 336, positionY: 336,
@ -261,7 +261,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('End', listFormCode)), ...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId, id: endId,
nodeId: endId, nodeId: endId,
title: 'İş Akışı Bitir1', title: translate('::ListForms.Workflow.Demo.End'),
positionX: 792, positionX: 792,
positionY: 336, positionY: 336,
} as WorkflowCriteriaDto, } as WorkflowCriteriaDto,
@ -470,7 +470,7 @@ function WizardStep6({
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full"> <div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back')}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
<Button <Button
@ -480,7 +480,7 @@ function WizardStep6({
icon={<FaArrowRight />} icon={<FaArrowRight />}
onClick={continueToTodo} onClick={continueToTodo}
> >
{translate('::Next') || 'Next'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -7,6 +7,7 @@ import {
collectLinks, collectLinks,
getNodeOutcomes, getNodeOutcomes,
outcomeLabel, outcomeLabel,
OUTCOME_KEYS,
type WorkflowLink, type WorkflowLink,
type WorkflowOutcome, type WorkflowOutcome,
} from '@/utils/workflow/workflowHelpers' } from '@/utils/workflow/workflowHelpers'
@ -93,6 +94,8 @@ export function WorkflowCanvas({
) )
const links = useMemo(() => collectLinks(arrowCriteria) as WorkflowLink[], [arrowCriteria]) const links = useMemo(() => collectLinks(arrowCriteria) as WorkflowLink[], [arrowCriteria])
const { translate } = useLocalization()
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== 'Delete') return if (event.key !== 'Delete') return
event.preventDefault() event.preventDefault()
@ -124,14 +127,14 @@ export function WorkflowCanvas({
> >
{pendingLink && ( {pendingLink && (
<div className="sticky left-2.5 top-2.5 z-50 m-2.5 inline-flex min-h-[34px] items-center rounded-md border border-[#8bb3f1] bg-[#eff6ff] px-3 text-[13px] text-blue-800 shadow-lg dark:border-blue-500/60 dark:bg-blue-900/40 dark:text-blue-100"> <div className="sticky left-2.5 top-2.5 z-50 m-2.5 inline-flex min-h-[34px] items-center rounded-md border border-[#8bb3f1] bg-[#eff6ff] px-3 text-[13px] text-blue-800 shadow-lg dark:border-blue-500/60 dark:bg-blue-900/40 dark:text-blue-100">
{outcomeLabel(pendingLink.outcome)} çıkışı seçildi. Hedef akışı adımına tıklayın. {translate('::ListForms.Workflow.PendingLinkHint', {
outcome: translate('::' + (outcomeLabel(pendingLink.outcome) ?? '')),
})}
</div> </div>
)} )}
{currentCriteria.length === 0 && ( {currentCriteria.length === 0 && (
<div className="sticky left-[18px] top-[18px] z-30 inline-grid max-w-[360px] gap-1 rounded-lg border border-[#cfd6e2] bg-white/95 p-3.5 text-[#475467] shadow-lg dark:border-gray-700 dark:bg-gray-900/95 dark:text-gray-300"> <div className="sticky left-[18px] top-[18px] z-30 inline-grid max-w-[360px] gap-1 rounded-lg border border-[#cfd6e2] bg-white/95 p-3.5 text-[#475467] shadow-lg dark:border-gray-700 dark:bg-gray-900/95 dark:text-gray-300">
<span> <span>{translate('::ListForms.Workflow.EmptyCanvasHint')}</span>
Üstteki butonlardan adım ekleyin, sonra çıkış etiketleriyle bağlantıları kurun.
</span>
</div> </div>
)} )}
<div <div
@ -252,11 +255,7 @@ export function WorkflowCanvas({
aria-hidden="true" aria-hidden="true"
> >
{links.map((link) => ( {links.map((link) => (
<ArrowLabel <ArrowLabel key={`${link.key}-label`} link={link} pendingLink={pendingLink} />
key={`${link.key}-label`}
link={link}
pendingLink={pendingLink}
/>
))} ))}
</svg> </svg>
</div> </div>
@ -387,7 +386,9 @@ function FlowNode({
onBeginLink(item.id, outcome.field) onBeginLink(item.id, outcome.field)
}} }}
> >
{outcome.label} {outcome.label.startsWith('ListForms.')
? translate('::' + outcome.label)
: outcome.label}
</span> </span>
))} ))}
</div> </div>
@ -503,13 +504,7 @@ function Arrow({
) )
} }
function ArrowLabel({ function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
link,
pendingLink,
}: {
link: WorkflowLink
pendingLink: PendingLink
}) {
if (!link.label) return null if (!link.label) return null
const route = buildArrowRoute(link.source, link.target, link.sourcePort) const route = buildArrowRoute(link.source, link.target, link.sourcePort)
@ -550,8 +545,8 @@ function ArrowLabel({
function linkTone(link: WorkflowLink) { function linkTone(link: WorkflowLink) {
const field = link.sourcePort?.field || '' const field = link.sourcePort?.field || ''
const label = link.label || '' const label = link.label || ''
if (field === 'nextOnReject' || label === 'Red') return 'reject' if (field === 'nextOnReject' || label === OUTCOME_KEYS.nextOnReject) return 'reject'
if (field === 'nextOnApprove' || label === 'Onay') return 'approve' if (field === 'nextOnApprove' || label === OUTCOME_KEYS.nextOnApprove) return 'approve'
if (field.startsWith('compareOutcomes:') || link.source.kind === 'Compare') return 'compare' if (field.startsWith('compareOutcomes:') || link.source.kind === 'Compare') return 'compare'
if (field === 'nextOnStart') return 'next' if (field === 'nextOnStart') return 'next'
return 'neutral' return 'neutral'

View file

@ -18,6 +18,8 @@ import {
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service' import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant' import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
@ -48,9 +50,10 @@ export function WorkflowCriteria({
onSubmit, onSubmit,
onDelete, onDelete,
}: WorkflowCriteriaProps) { }: WorkflowCriteriaProps) {
const { translate } = useLocalization()
const setField = (name: string, value: unknown) => onChange({ ...formValues, [name]: value }) const setField = (name: string, value: unknown) => onChange({ ...formValues, [name]: value })
const targetOptions = [ const targetOptions = [
{ value: '', label: 'Bağlantı yok' }, { value: '', label: translate('::ListForms.Workflow.NoConnection') },
...criteria ...criteria
.filter((item) => item.id !== formValues.id) .filter((item) => item.id !== formValues.id)
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })), .map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
@ -164,7 +167,6 @@ export function WorkflowCriteria({
/> />
) )
const closeDialog = () => onSelect('') const closeDialog = () => onSelect('')
const { translate } = useLocalization()
return ( return (
<> <>
@ -189,7 +191,7 @@ export function WorkflowCriteria({
{criteria.map((item) => { {criteria.map((item) => {
const isSelected = item.id === selectedId const isSelected = item.id === selectedId
const connectionSummary = criteriaConnectionSummary(item, criteria) const connectionSummary = criteriaConnectionSummary(item, criteria, translate)
return ( return (
<Tr <Tr
@ -233,7 +235,9 @@ export function WorkflowCriteria({
'::' + kindOptions.find((option) => option.value === item.kind)?.value, '::' + kindOptions.find((option) => option.value === item.kind)?.value,
)} )}
</Td> </Td>
<Td className="min-w-[220px] break-words">{criteriaSummaryContent(item)}</Td> <Td className="min-w-[220px] break-words">
{criteriaSummaryContent(item, translate)}
</Td>
<Td className="min-w-[220px] break-words">{connectionSummary}</Td> <Td className="min-w-[220px] break-words">{connectionSummary}</Td>
</Tr> </Tr>
) )
@ -250,7 +254,10 @@ export function WorkflowCriteria({
<FormItem asterisk label={translate('::App.Platform.Type')}> <FormItem asterisk label={translate('::App.Platform.Type')}>
<SelectField <SelectField
required required
options={kindOptions} options={kindOptions.map((option) => ({
...option,
label: translate('::' + option.label),
}))}
value={formValues.kind} value={formValues.kind}
onChange={(value) => setField('kind', value)} onChange={(value) => setField('kind', value)}
/> />
@ -263,10 +270,7 @@ export function WorkflowCriteria({
/> />
</FormItem> </FormItem>
{(formValues.kind === 'Approval' || formValues.kind === 'Inform') && ( {(formValues.kind === 'Approval' || formValues.kind === 'Inform') && (
<FormItem <FormItem asterisk label={translate('::App.Listform.ListformField.Approver')}>
asterisk
label={translate('::App.Listform.ListformField.Approver')}
>
<MultiSelectField <MultiSelectField
required required
options={userList} options={userList}
@ -334,7 +338,9 @@ export function WorkflowCriteria({
setField('compareOutcomes', [ setField('compareOutcomes', [
...(formValues.compareOutcomes || []), ...(formValues.compareOutcomes || []),
emptyCompareOutcome1( emptyCompareOutcome1(
`Durum ${(formValues.compareOutcomes || []).length + 1}`, translate('::ListForms.Workflow.OutcomeN', {
index: (formValues.compareOutcomes || []).length + 1,
}),
defaultCompareColumn, defaultCompareColumn,
), ),
]) ])
@ -364,7 +370,7 @@ export function WorkflowCriteria({
<Input <Input
required required
value={outcome.label} value={outcome.label}
aria-label="Durum adı zorunlu" aria-label={translate('::ListForms.Workflow.OutcomeNameRequired')}
className="flex-[5]" className="flex-[5]"
onChange={(event) => onChange={(event) =>
updateCompareOutcome(index, { updateCompareOutcome(index, {
@ -439,7 +445,7 @@ export function WorkflowCriteria({
className="flex-[1]" className="flex-[1]"
onClick={() => removeCompareCondition(index, conditionIndex)} onClick={() => removeCompareCondition(index, conditionIndex)}
> >
Sil {translate('::App.Platform.Delete')}
</Button> </Button>
<Button <Button
@ -571,7 +577,7 @@ function MultiSelectField({
) )
} }
function criteriaSummaryContent(item: WorkflowCriteriaDto) { function criteriaSummaryContent(item: WorkflowCriteriaDto, translate: TranslateFn) {
if (item.kind === 'Compare') { if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes || [] const outcomes = item.compareOutcomes || []
if (!outcomes.length) return '-' if (!outcomes.length) return '-'
@ -580,7 +586,9 @@ function criteriaSummaryContent(item: WorkflowCriteriaDto) {
<ul className="m-0 grid gap-1"> <ul className="m-0 grid gap-1">
{outcomes.map((outcome, index: number) => ( {outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || 'outcome'}-${index}`}> <li key={`${outcome.label || 'outcome'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '} <strong>
{outcome.label || translate('::ListForms.Workflow.OutcomeN', { index: index + 1 })}:
</strong>{' '}
{compareOutcomeRuleText(outcome)} {compareOutcomeRuleText(outcome)}
</li> </li>
))} ))}
@ -591,7 +599,11 @@ function criteriaSummaryContent(item: WorkflowCriteriaDto) {
return criteriaSummary(item) return criteriaSummary(item)
} }
function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) { function criteriaConnectionSummary(
item: WorkflowCriteriaDto,
criteria: WorkflowCriteriaDto[],
translate: TranslateFn,
) {
if (item.kind === 'Compare') { if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes || [] const outcomes = item.compareOutcomes || []
if (!outcomes.length) return '-' if (!outcomes.length) return '-'
@ -600,7 +612,9 @@ function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: Workflow
<ul className="m-0 grid gap-1"> <ul className="m-0 grid gap-1">
{outcomes.map((outcome, index: number) => ( {outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || 'target'}-${index}`}> <li key={`${outcome.label || 'target'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '} <strong>
{outcome.label || translate('::ListForms.Workflow.OutcomeN', { index: index + 1 })}:
</strong>{' '}
{targetTitle(criteria, outcome.targetId)} {targetTitle(criteria, outcome.targetId)}
</li> </li>
))} ))}
@ -612,10 +626,12 @@ function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: Workflow
return ( return (
<ul className="m-0 grid gap-1"> <ul className="m-0 grid gap-1">
<li> <li>
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)} <strong>{translate('::ListForms.Workflow.Approve')}:</strong>{' '}
{targetTitle(criteria, item.nextOnApprove)}
</li> </li>
<li> <li>
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)} <strong>{translate('::ListForms.Workflow.Reject')}:</strong>{' '}
{targetTitle(criteria, item.nextOnReject)}
</li> </li>
</ul> </ul>
) )

View file

@ -197,7 +197,7 @@ function DesignerToolbar({
shape="round" shape="round"
className={classNames(designerButtonClass, designerNeutralColorClass)} className={classNames(designerButtonClass, designerNeutralColorClass)}
disabled={busy} disabled={busy}
title="Demo akışı yükle" title={translate('::ListForms.Workflow.LoadDemoFlow')}
onClick={onResetDemo} onClick={onResetDemo}
> >
<FiRefreshCw /> <FiRefreshCw />
@ -209,7 +209,7 @@ function DesignerToolbar({
shape="round" shape="round"
className={classNames(designerButtonClass, designerAccentColorClass)} className={classNames(designerButtonClass, designerAccentColorClass)}
disabled={busy || currentCriteria.length === 0} disabled={busy || currentCriteria.length === 0}
title="Düğümleri okunabilir şekilde yerleştir" title={translate('::ListForms.Workflow.FitLayout')}
onClick={onFitLayout} onClick={onFitLayout}
> >
<FiMaximize2 /> <FiMaximize2 />
@ -220,7 +220,7 @@ function DesignerToolbar({
variant="solid" variant="solid"
shape="round" shape="round"
className={classNames(designerIconButtonClass, designerAccentColorClass)} className={classNames(designerIconButtonClass, designerAccentColorClass)}
title="Yakınlaştır" title={translate('::App.Platform.ZoomIn')}
onClick={onZoomIn} onClick={onZoomIn}
> >
<FiZoomIn /> <FiZoomIn />
@ -230,7 +230,7 @@ function DesignerToolbar({
variant="solid" variant="solid"
shape="round" shape="round"
className={classNames(designerIconButtonClass, designerAccentColorClass)} className={classNames(designerIconButtonClass, designerAccentColorClass)}
title="Uzaklaştır" title={translate('::App.Platform.ZoomOut')}
onClick={onZoomOut} onClick={onZoomOut}
> >
<FiZoomOut /> <FiZoomOut />
@ -269,7 +269,11 @@ function DesignerTabs({
const { translate } = useLocalization() const { translate } = useLocalization()
return ( return (
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı"> <div
className="inline-flex gap-1 rounded-lg"
role="tablist"
aria-label={translate('::ListForms.Workflow.FlowDesign')}
>
<Button <Button
type="button" type="button"
variant="plain" variant="plain"

View file

@ -222,7 +222,7 @@ const OrganizationUnits = () => {
} }
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{translate('::Kaydet')} {translate('::Save')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -234,7 +234,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -291,7 +291,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -327,7 +327,7 @@ const OrganizationUnits = () => {
await ouPut({ id, displayName: name }) await ouPut({ id, displayName: name })
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{translate('::Kaydet')} {translate('::Save')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -338,7 +338,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -360,7 +360,7 @@ const OrganizationUnits = () => {
await ouPutMove(id, newParentId) await ouPutMove(id, newParentId)
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{translate('::Kaydet')} {translate('::Save')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -371,7 +371,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -428,8 +428,8 @@ const OrganizationUnits = () => {
<div className="file-actions"> <div className="file-actions">
<div className="flex gap-1 folderFileActions"> <div className="flex gap-1 folderFileActions">
<Button <Button
size='xs' size="xs"
variant='plain' variant="plain"
shape="circle" shape="circle"
className="!h-6 !w-6 !bg-blue-50 !p-0 text-blue-600 hover:!bg-blue-100 hover:text-blue-700 dark:!bg-blue-900/30 dark:text-blue-300 dark:hover:!bg-blue-800/60 dark:hover:text-blue-100" className="!h-6 !w-6 !bg-blue-50 !p-0 text-blue-600 hover:!bg-blue-100 hover:text-blue-700 dark:!bg-blue-900/30 dark:text-blue-300 dark:hover:!bg-blue-800/60 dark:hover:text-blue-100"
icon={<FaUserPlus className="text-xs" />} icon={<FaUserPlus className="text-xs" />}
@ -437,8 +437,8 @@ const OrganizationUnits = () => {
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')} title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
></Button> ></Button>
<Button <Button
size='xs' size="xs"
variant='plain' variant="plain"
shape="circle" shape="circle"
className="!h-6 !w-6 !bg-teal-50 !p-0 text-teal-600 hover:!bg-teal-100 hover:text-teal-700 dark:!bg-teal-900/30 dark:text-teal-300 dark:hover:!bg-teal-800/60 dark:hover:text-teal-100" className="!h-6 !w-6 !bg-teal-50 !p-0 text-teal-600 hover:!bg-teal-100 hover:text-teal-700 dark:!bg-teal-900/30 dark:text-teal-300 dark:hover:!bg-teal-800/60 dark:hover:text-teal-100"
icon={<FaEdit className="text-xs" />} icon={<FaEdit className="text-xs" />}
@ -446,8 +446,8 @@ const OrganizationUnits = () => {
title={translate('::Abp.Identity.OrganizationUnit.Rename')} title={translate('::Abp.Identity.OrganizationUnit.Rename')}
></Button> ></Button>
<Button <Button
size='xs' size="xs"
variant='plain' variant="plain"
shape="circle" shape="circle"
className="!h-6 !w-6 !bg-red-50 !p-0 text-red-600 hover:!bg-red-100 hover:text-red-700 dark:!bg-red-900/30 dark:text-red-300 dark:hover:!bg-red-800/60 dark:hover:text-red-100" className="!h-6 !w-6 !bg-red-50 !p-0 text-red-600 hover:!bg-red-100 hover:text-red-700 dark:!bg-red-900/30 dark:text-red-300 dark:hover:!bg-red-800/60 dark:hover:text-red-100"
icon={<FaTrashAlt className="text-xs" />} icon={<FaTrashAlt className="text-xs" />}
@ -562,9 +562,9 @@ const OrganizationUnits = () => {
<THead> <THead>
<Tr> <Tr>
<Th></Th> <Th></Th>
<Th>Adı Soyadı</Th> <Th>{translate('::App.Platform.FullName')}</Th>
<Th>E-Posta</Th> <Th>{translate('::Abp.Account.EmailAddress')}</Th>
<Th>Durum</Th> <Th>{translate('::App.Platform.Status')}</Th>
</Tr> </Tr>
</THead> </THead>
)} )}
@ -633,7 +633,7 @@ const OrganizationUnits = () => {
<THead> <THead>
<Tr> <Tr>
<Th></Th> <Th></Th>
<Th>Rol</Th> <Th>{translate('::App.Platform.Role')}</Th>
</Tr> </Tr>
</THead> </THead>
)} )}
@ -786,7 +786,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -857,7 +857,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -935,7 +935,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -984,7 +984,7 @@ const OrganizationUnits = () => {
} catch (error) { } catch (error) {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
Hata {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',

View file

@ -89,15 +89,15 @@ const General = () => {
const maxFileSize = 2000000 const maxFileSize = 2000000
if (fileList.length >= 1) { if (fileList.length >= 1) {
return `Sadece bir dosya seçebilirsiniz` return translate('::App.Profile.OnlyOneFile')
} }
if (files) { if (files) {
for (const f of files) { for (const f of files) {
if (!allowedFileType.includes(f.type)) { if (!allowedFileType.includes(f.type)) {
valid = '.jpg, .jpeg, .gif veya .png yükleyebilirsiniz' valid = translate('::App.Profile.AllowedImageTypes')
} else if (f.size >= maxFileSize) { } else if (f.size >= maxFileSize) {
valid = 'En fazla 2mb dosya yükleyebilirsiniz' valid = translate('::App.Profile.MaxFileSize')
} }
} }
} }
@ -122,7 +122,7 @@ const General = () => {
avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`, avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`,
}) })
toast.push(<Notification title={'Profil güncellendi'} type="success" />, { toast.push(<Notification title={translate('::App.Profile.Updated')} type="success" />, {
placement: 'bottom-end', placement: 'bottom-end',
}) })
} else { } else {
@ -178,31 +178,31 @@ const General = () => {
const educationOptions: SelectBoxOption[] = [ const educationOptions: SelectBoxOption[] = [
{ {
value: 'İlkokul', value: 'İlkokul',
label: translate('::App.EducationLevel.Primary') || 'İlkokul', label: translate('::App.EducationLevel.Primary'),
}, },
{ {
value: 'Ortaokul', value: 'Ortaokul',
label: translate('::App.EducationLevel.MiddleSchool') || 'Ortaokul', label: translate('::App.EducationLevel.MiddleSchool'),
}, },
{ {
value: 'Lise', value: 'Lise',
label: translate('::App.EducationLevel.HighSchool') || 'Lise', label: translate('::App.EducationLevel.HighSchool'),
}, },
{ {
value: 'Ön Lisans', value: 'Ön Lisans',
label: translate('::App.EducationLevel.Associate') || 'Ön Lisans', label: translate('::App.EducationLevel.Associate'),
}, },
{ {
value: 'Lisans', value: 'Lisans',
label: translate('::App.EducationLevel.Bachelor') || 'Lisans', label: translate('::App.EducationLevel.Bachelor'),
}, },
{ {
value: 'Yüksek Lisans', value: 'Yüksek Lisans',
label: translate('::App.EducationLevel.Master') || 'Yüksek Lisans', label: translate('::App.EducationLevel.Master'),
}, },
{ {
value: 'Doktora', value: 'Doktora',
label: translate('::App.EducationLevel.PhD') || 'Doktora', label: translate('::App.EducationLevel.PhD'),
}, },
] ]

View file

@ -84,7 +84,7 @@ const CreateTenantFromOrderDialog = ({
toast.push( toast.push(
<Notification type="success" duration={2500}> <Notification type="success" duration={2500}>
{translate('::App.Platform.Success') || 'Tenant kaydedildi'} {translate('::App.Platform.Success')}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { placement: 'bottom-end' },
) )
@ -95,7 +95,7 @@ const CreateTenantFromOrderDialog = ({
(error as any)?.response?.data?.error?.message ?? (error as any)?.response?.data?.error?.message ??
(error as any)?.response?.data?.message ?? (error as any)?.response?.data?.message ??
(error as any)?.message ?? (error as any)?.message ??
'Islem basarisiz' translate('::App.Platform.OperationFailed')
toast.push( toast.push(
<Notification type="danger" duration={5000}> <Notification type="danger" duration={5000}>

View file

@ -102,7 +102,7 @@ function TenantConnectionString({
} catch { } catch {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
{'Hata'} {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -136,7 +136,7 @@ function TenantConnectionString({
} catch { } catch {
toast.push( toast.push(
<Notification type="danger" duration={2000}> <Notification type="danger" duration={2000}>
{'Hata'} {translate('::App.Platform.Error')}
</Notification>, </Notification>,
{ {
placement: 'bottom-end', placement: 'bottom-end',
@ -187,7 +187,7 @@ function TenantConnectionString({
(error as any)?.response?.data?.error?.message ?? (error as any)?.response?.data?.error?.message ??
(error as any)?.response?.data?.message ?? (error as any)?.response?.data?.message ??
(error as any)?.message ?? (error as any)?.message ??
'İşlem başarısız' translate('::App.Platform.OperationFailed')
toast.push( toast.push(
<Notification type="warning" duration={10000}> <Notification type="warning" duration={10000}>

Some files were not shown because too many files have changed in this diff Show more