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' && (
<label className="relative"> <div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
<FaSearch className="absolute left-3 top-3 text-slate-400" /> <label className="relative">
<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)} /> <FaSearch className="absolute left-3 top-3 text-slate-400" />
</label> <input
<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)}> 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"
<option value="Tümü">Tüm kategoriler</option> placeholder={translate('::App.StyleModal.SearchClasses')}
{Object.keys(STYLE_GROUPS).map((name) => <option key={name} value={name}>{name}</option>)} value={search}
</select> onChange={(event) => setSearch(event.target.value)}
</div>} />
{mode === 'class' && <div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700"> </label>
<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() }} /> <select
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>Ekle</Button> className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
</div>} value={category}
onChange={(event) => setCategory(event.target.value as typeof category)}
>
<option value="__all__">{translate('::App.StyleModal.AllCategories')}</option>
{Object.keys(STYLE_GROUPS).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</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"> <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">{propertyName} değerini elle düzenle</label> <label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">
<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)} /> {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> </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"> {mode === 'class' && (
{classes.map((className) => ( <div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
<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> {classes.map((className) => (
))} <button
</div>} 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"
@ -174,422 +174,289 @@ const PublicLayout = () => {
<DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}> <DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}>
<ScrollContext.Provider value={scrolled}> <ScrollContext.Provider value={scrolled}>
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
{/* HEADER */} {/* HEADER */}
<header <header
className={`fixed w-full z-50 transition-all duration-500 ${ className={`fixed w-full z-50 transition-all duration-500 ${
scrolled scrolled
? 'bg-gray-950 shadow-lg shadow-black/30 py-2 border-b border-white/10' ? 'bg-gray-950 shadow-lg shadow-black/30 py-2 border-b border-white/10'
: 'bg-gray-950/80 backdrop-blur-sm py-4 border-b border-white/5' : 'bg-gray-950/80 backdrop-blur-sm py-4 border-b border-white/5'
}`}
>
<div className="container mx-auto px-6 relative grid grid-cols-[auto_1fr_auto] items-center lg:grid-cols-[1fr_auto] xl:grid-cols-[auto_1fr_auto]">
<Logo
mode={isDarkMode ? 'dark' : 'light'}
className="relative z-10 max-w-[190px] overflow-hidden lg:hidden xl:block xl:max-w-none"
imgClass="h-10 w-auto object-contain xl:h-auto"
/>
{/* Desktop / tablet navigation */}
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
{navLinks
.filter((l) => !isLoginLink(l.resourceKey))
.map((link) => {
const active = isActiveLink(link.path)
const baseClass =
'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'
return link.path ? (
<Link key={link.path} to={link.path} className={`${baseClass} ${activeClass}`}>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className={
active
? 'text-blue-400'
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
}
/>
)}
{link.name}
<span
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
}`}
/>
</Link>
) : (
<Button
key={link.name}
onClick={link.action}
variant="plain"
size="sm"
className={`${baseClass} ${activeClass}`}
>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className="text-gray-400 group-hover:text-blue-400 transition-colors"
/>
)}
{link.name}
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
</Button>
)
})}
</nav>
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
{demoButton('rounded-lg')}
<div className="h-5 w-px bg-white/20" />
<div className="flex h-9 w-9 items-center justify-center">
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
<div className="h-5 w-px bg-white/20" />
{themeToggle}
<div className="h-5 w-px bg-white/20" />
{navLinks
.filter((l) => isLoginLink(l.resourceKey))
.map((link) =>
link.path ? (
<Button
key={link.path}
type="button"
variant="solid"
shape="round"
onClick={() => navigate(link.path!)}
>
{link.name}
</Button>
) : null,
)}
</div>
{/* Mobile Menu 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"
onClick={toggleMenu}
aria-label="Toggle menu"
aria-expanded={isOpen}
icon={
isOpen ? (
<LuX size={20} strokeWidth={2} />
) : (
<LuMenu size={20} strokeWidth={2} />
)
}
variant="plain"
size="xs"
/>
</div>
{/* Mobile Navigation */}
<div
className={`lg:hidden transition-all duration-300 ease-in-out ${
isOpen
? 'max-h-screen overflow-visible opacity-100'
: 'max-h-0 overflow-hidden opacity-0'
}`} }`}
> >
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md"> <div className="container mx-auto px-6 relative grid grid-cols-[auto_1fr_auto] items-center lg:grid-cols-[1fr_auto] xl:grid-cols-[auto_1fr_auto]">
<div className="container mx-auto px-6 py-3"> <Logo
<nav className="flex flex-col gap-1"> mode={isDarkMode ? 'dark' : 'light'}
<div className="lg:hidden flex flex-col gap-1"> className="relative z-10 max-w-[190px] overflow-hidden lg:hidden xl:block xl:max-w-none"
{navLinks.map((link) => { imgClass="h-10 w-auto object-contain xl:h-auto"
const active = isActiveLink(link.path) />
const isLogin = isLoginLink(link.resourceKey)
if (isLogin) { {/* Desktop / tablet navigation */}
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
{navLinks
.filter((l) => !isLoginLink(l.resourceKey))
.map((link) => {
const active = isActiveLink(link.path)
const baseClass =
'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'
return link.path ? (
<Link
key={link.path}
to={link.path}
className={`${baseClass} ${activeClass}`}
>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className={
active
? 'text-blue-400'
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
}
/>
)}
{link.name}
<span
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
}`}
/>
</Link>
) : (
<Button
key={link.name}
onClick={link.action}
variant="plain"
size="sm"
className={`${baseClass} ${activeClass}`}
>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className="text-gray-400 group-hover:text-blue-400 transition-colors"
/>
)}
{link.name}
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
</Button>
)
})}
</nav>
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
{demoButton('rounded-lg')}
<div className="h-5 w-px bg-white/20" />
<div className="flex h-9 w-9 items-center justify-center">
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
<div className="h-5 w-px bg-white/20" />
{themeToggle}
<div className="h-5 w-px bg-white/20" />
{navLinks
.filter((l) => isLoginLink(l.resourceKey))
.map((link) =>
link.path ? (
<Button
key={link.path}
type="button"
variant="solid"
shape="round"
onClick={() => navigate(link.path!)}
>
{link.name}
</Button>
) : null,
)}
</div>
{/* Mobile Menu 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"
onClick={toggleMenu}
aria-label={translate('::App.Platform.ToggleMenu')}
aria-expanded={isOpen}
icon={
isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />
}
variant="plain"
size="xs"
/>
</div>
{/* Mobile Navigation */}
<div
className={`lg:hidden transition-all duration-300 ease-in-out ${
isOpen
? 'max-h-screen overflow-visible opacity-100'
: 'max-h-0 overflow-hidden opacity-0'
}`}
>
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md">
<div className="container mx-auto px-6 py-3">
<nav className="flex flex-col gap-1">
<div className="lg:hidden flex flex-col gap-1">
{navLinks.map((link) => {
const active = isActiveLink(link.path)
const isLogin = isLoginLink(link.resourceKey)
if (isLogin) {
return link.path ? (
<Link
key={link.path}
to={link.path}
onClick={toggleMenu}
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
>
{link.name}
</Link>
) : null
}
return link.path ? ( return link.path ? (
<Link <Link
key={link.path} key={link.path}
to={link.path} to={link.path}
onClick={toggleMenu} onClick={toggleMenu}
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors" className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${
active
? 'bg-white/10 text-white'
: 'text-gray-300 hover:bg-white/5 hover:text-white'
}`}
> >
{link.icon && (
<link.icon
size={16}
strokeWidth={1.75}
className={active ? 'text-blue-400' : 'text-gray-400'}
/>
)}
{link.name} {link.name}
</Link> </Link>
) : null ) : (
} <Button
return link.path ? ( key={link.name}
<Link onClick={() => {
key={link.path} link.action?.()
to={link.path} toggleMenu()
onClick={toggleMenu} }}
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${ variant="plain"
active size="sm"
? 'bg-white/10 text-white' className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
: 'text-gray-300 hover:bg-white/5 hover:text-white' >
}`} {link.icon && (
> <link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
{link.icon && ( )}
<link.icon {link.name}
size={16} </Button>
strokeWidth={1.75} )
className={active ? 'text-blue-400' : 'text-gray-400'} })}
/>
)}
{link.name}
</Link>
) : (
<Button
key={link.name}
onClick={() => {
link.action?.()
toggleMenu()
}}
variant="plain"
size="sm"
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
>
{link.icon && (
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
)}
{link.name}
</Button>
)
})}
</div>
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-1">
{demoButton('rounded-lg', toggleMenu)}
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
</div>
<div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
{navLinks
.filter((l) => isLoginLink(l.resourceKey))
.map((link) =>
link.path ? (
<Link
key={link.path}
to={link.path}
onClick={toggleMenu}
className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
>
{link.name}
</Link>
) : null,
)}
</div> </div>
</div> <div className="mt-1 border-t border-white/10 pt-2 pb-1">
</nav> <div className="flex items-center justify-between gap-3">
</div> <div className="flex items-center gap-1">
</div> {demoButton('rounded-lg', toggleMenu)}
</div> <div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
</header> <LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
<main className="flex-grow"> </div>
<View /> <div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
</main> {navLinks
.filter((l) => isLoginLink(l.resourceKey))
<footer className="bg-gray-900 text-white pt-16 pb-8"> .map((link) =>
<div className="container mx-auto px-4"> link.path ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"> <Link
<div> key={link.path}
<Logo mode={isDarkMode ? 'dark' : 'light'} /> to={link.path}
<p className="mt-4 text-gray-400 text-sm"> onClick={toggleMenu}
{translate('::Public.footer.companyInfo')} className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
</p> >
<div className="flex space-x-4 mt-6"> {link.name}
<a </Link>
href="https://facebook.com/sozsoft" ) : null,
target="_blank" )}
rel="noopener noreferrer" </div>
className="text-gray-400 hover:text-white transition-colors" </div>
> </nav>
<FaFacebook size={20} />
</a>
<a
href="https://twitter.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaTwitter size={20} />
</a>
<a
href="https://linkedin.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaLinkedin size={20} />
</a>
<a
href="https://instagram.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaInstagram size={20} />
</a>
</div> </div>
</div> </div>
{/* Quick Links */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::Public.footer.quickLinksTitle')}
</h3>
<ul className="space-y-2">
<li>
<Link
to={ROUTES_ENUM.public.home}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Home')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.products}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Orders')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Services')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.About')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.blog}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.BlogManagement')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.contact}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Contact')}
</Link>
</li>
</ul>
</div>
{/* Services */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::Public.services.title')}
</h3>
<ul className="space-y-2">
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.software.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.web.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.mobile.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.database.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.integration.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.consulting.title')}
</Link>
</li>
</ul>
</div>
{/* Contact Info */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">{translate('::App.Contact')}</h3>
<ul className="space-y-3">
<li className="flex items-start space-x-3">
<LuMapPin
size={18}
strokeWidth={1.75}
className="text-gray-400 mt-1 flex-shrink-0"
/>
<span className="text-gray-400">{translate('::Public.footer.address')}</span>
</li>
<li className="flex items-center space-x-3">
<LuPhone size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" />
<a
href="tel:+905447697638"
className="text-gray-400 hover:text-white transition-colors"
>
+90 (544) 769 7 638
</a>
</li>
<li className="flex items-center space-x-3">
<LuMail size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" />
<a
href="mailto:destek@sozsoft.com"
className="text-gray-400 hover:text-white transition-colors"
>
destek@sozsoft.com
</a>
</li>
</ul>
</div>
</div> </div>
</header>
<div className="border-t border-gray-800 mt-12 pt-8"> <main className="flex-grow">
<div className="flex flex-col md:flex-row justify-between items-center"> <View />
<p className="text-gray-400 text-sm"> </main>
&copy; {currentYear} Sözsoft Platform. {translate('::Public.footer.copyright')}
</p> <footer className="bg-gray-900 text-white pt-16 pb-8">
<div className="mt-4 md:mt-0"> <div className="container mx-auto px-4">
<ul className="flex space-x-6 text-sm"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div>
<Logo mode={isDarkMode ? 'dark' : 'light'} />
<p className="mt-4 text-gray-400 text-sm">
{translate('::Public.footer.companyInfo')}
</p>
<div className="flex space-x-4 mt-6">
<a
href="https://facebook.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaFacebook size={20} />
</a>
<a
href="https://twitter.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaTwitter size={20} />
</a>
<a
href="https://linkedin.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaLinkedin size={20} />
</a>
<a
href="https://instagram.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaInstagram size={20} />
</a>
</div>
</div>
{/* Quick Links */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::Public.footer.quickLinksTitle')}
</h3>
<ul className="space-y-2">
<li> <li>
<Link <Link
to={ROUTES_ENUM.public.about} to={ROUTES_ENUM.public.home}
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors"
> >
{translate('::Public.footer.privacyPolicy')} {translate('::App.Home')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.products}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Orders')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Services')}
</Link> </Link>
</li> </li>
<li> <li>
@ -597,17 +464,160 @@ const PublicLayout = () => {
to={ROUTES_ENUM.public.about} to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors"
> >
{translate('::Public.footer.termsOfUse')} {translate('::App.About')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.blog}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.BlogManagement')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.contact}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Contact')}
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
{/* Services */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::Public.services.title')}
</h3>
<ul className="space-y-2">
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.software.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.web.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.mobile.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.database.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.integration.title')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.services.consulting.title')}
</Link>
</li>
</ul>
</div>
{/* Contact Info */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::App.Contact')}
</h3>
<ul className="space-y-3">
<li className="flex items-start space-x-3">
<LuMapPin
size={18}
strokeWidth={1.75}
className="text-gray-400 mt-1 flex-shrink-0"
/>
<span className="text-gray-400">{translate('::Public.footer.address')}</span>
</li>
<li className="flex items-center space-x-3">
<LuPhone
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a
href="tel:+905447697638"
className="text-gray-400 hover:text-white transition-colors"
>
+90 (544) 769 7 638
</a>
</li>
<li className="flex items-center space-x-3">
<LuMail
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a
href="mailto:destek@sozsoft.com"
className="text-gray-400 hover:text-white transition-colors"
>
destek@sozsoft.com
</a>
</li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-12 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center">
<p className="text-gray-400 text-sm">
&copy; {currentYear} Sözsoft Platform. {translate('::Public.footer.copyright')}
</p>
<div className="mt-4 md:mt-0">
<ul className="flex space-x-6 text-sm">
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.footer.privacyPolicy')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::Public.footer.termsOfUse')}
</Link>
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
</div> </footer>
</footer>
{/* Demo Modal */} {/* Demo Modal */}
<Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} /> <Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
</div> </div>
</ScrollContext.Provider> </ScrollContext.Provider>

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'
@ -35,15 +42,15 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
const billingCycleLabel = const billingCycleLabel =
order?.billingCycle === 'monthly' order?.billingCycle === 'monthly'
? translate('::Public.products.billingcycle.monthly') ? translate('::Public.products.billingcycle.monthly')
: order?.billingCycle === 'yearly' : order?.billingCycle === 'yearly'
? translate('::Public.products.billingcycle.yearly') ? translate('::Public.products.billingcycle.yearly')
: '-' : '-'
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

@ -1,13 +1,13 @@
import { import {
forwardRef, forwardRef,
useState, useState,
useRef, useRef,
useCallback, useCallback,
useEffect, useEffect,
useId, useId,
type KeyboardEvent, type KeyboardEvent,
type ChangeEvent, type ChangeEvent,
type ReactNode, type ReactNode,
} from 'react' } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { useConfig } from '../ConfigProvider' import { useConfig } from '../ConfigProvider'
@ -16,386 +16,348 @@ 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 ──────────────────────────────────────────────────────────────────
export interface AutoCompleteOption { export interface AutoCompleteOption {
label: string label: string
value: string value: string
disabled?: boolean disabled?: boolean
/** Özel veri (filtreleme / render için) */ /** Özel veri (filtreleme / render için) */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any data?: any
} }
export interface AutoCompleteProps extends CommonProps { export interface AutoCompleteProps extends CommonProps {
/** Statik seçenek listesi */ /** Statik seçenek listesi */
options?: AutoCompleteOption[] options?: AutoCompleteOption[]
/** Kontrollü input değeri */ /** Kontrollü input değeri */
value?: string value?: string
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: string defaultValue?: string
/** Placeholder */ /** Placeholder */
placeholder?: string placeholder?: string
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Geçersiz (kırmızı kenarlık) */ /** Geçersiz (kırmızı kenarlık) */
invalid?: boolean invalid?: boolean
/** Input boyutu */ /** Input boyutu */
size?: TypeAttributes.ControlSize size?: TypeAttributes.ControlSize
/** Async seçenek yükleme - (inputValue) => Promise<AutoCompleteOption[]> */ /** Async seçenek yükleme - (inputValue) => Promise<AutoCompleteOption[]> */
fetchOptions?: (query: string) => Promise<AutoCompleteOption[]> fetchOptions?: (query: string) => Promise<AutoCompleteOption[]>
/** Async istekleri geciktir (ms). Varsayılan: 300 */ /** Async istekleri geciktir (ms). Varsayılan: 300 */
debounce?: number debounce?: number
/** Seçenek özelleştirme render */ /** Seçenek özelleştirme render */
renderOption?: (option: AutoCompleteOption, active: boolean) => ReactNode renderOption?: (option: AutoCompleteOption, active: boolean) => ReactNode
/** "Seçenek bulunamadı" metni */ /** "Seçenek bulunamadı" metni */
noOptionsText?: string noOptionsText?: string
/** Yükleniyor metni */ /** Yükleniyor metni */
loadingText?: string loadingText?: string
/** Input değeri değiştiğinde */ /** Input değeri değiştiğinde */
onInputChange?: (value: string) => void onInputChange?: (value: string) => void
/** Bir seçenek seçildiğinde */ /** Bir seçenek seçildiğinde */
onSelect?: (option: AutoCompleteOption) => void onSelect?: (option: AutoCompleteOption) => void
/** Temizle butonu göster */ /** Temizle butonu göster */
clearable?: boolean clearable?: boolean
/** Input adı */ /** Input adı */
name?: string name?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
field?: any field?: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
form?: any form?: any
/** Minimum karakter sayısı (async için). Varsayılan: 1 */ /** Minimum karakter sayısı (async için). Varsayılan: 1 */
minChars?: number minChars?: number
} }
// ── Yardımcılar ───────────────────────────────────────────────────────────── // ── Yardımcılar ─────────────────────────────────────────────────────────────
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, const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
delay: number, return useCallback(
) { (...args: Parameters<T>) => {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null) if (timerRef.current) clearTimeout(timerRef.current)
return useCallback( timerRef.current = setTimeout(() => fn(...args), delay)
(...args: Parameters<T>) => { },
if (timerRef.current) clearTimeout(timerRef.current) [fn, delay],
timerRef.current = setTimeout(() => fn(...args), delay) )
},
[fn, delay],
)
} }
// ── 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,
options = [], options = [],
value: valueProp, value: valueProp,
defaultValue = '', defaultValue = '',
placeholder, placeholder,
disabled = false, disabled = false,
invalid = false, invalid = false,
size, size,
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,
name, name,
field, field,
form, form,
minChars = 1, minChars = 1,
...rest ...rest
} = props } = props
const isControlled = valueProp !== undefined const isControlled = valueProp !== undefined
const [inputValue, setInputValue] = useState( const [inputValue, setInputValue] = useState(
field?.value ?? (isControlled ? valueProp! : defaultValue), field?.value ?? (isControlled ? valueProp! : defaultValue),
) )
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)
const listRef = useRef<HTMLUListElement>(null) const listRef = useRef<HTMLUListElement>(null)
const listboxId = useId() const listboxId = useId()
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
useEffect(() => { useEffect(() => {
if (isControlled) setInputValue(valueProp!) if (isControlled) setInputValue(valueProp!)
}, [isControlled, valueProp]) }, [isControlled, valueProp])
// Sync field value (Formik / RHF) // Sync field value (Formik / RHF)
useEffect(() => { useEffect(() => {
if (field?.value !== undefined) setInputValue(field.value) if (field?.value !== undefined) setInputValue(field.value)
}, [field?.value]) }, [field?.value])
// 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 && setOpen(false)
!containerRef.current.contains(e.target as Node) setActiveIndex(-1)
) { }
setOpen(false) }
setActiveIndex(-1) document.addEventListener('mousedown', handler)
} return () => document.removeEventListener('mousedown', handler)
} }, [])
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [])
// Statik filtreleme // Statik filtreleme
const filterStatic = useCallback( const filterStatic = useCallback(
(query: string) => { (query: string) => {
if (!fetchOptions) { if (!fetchOptions) {
const result = query ? defaultFilter(options, query) : options const result = query ? defaultFilter(options, query) : options
setFilteredOptions(result) setFilteredOptions(result)
} }
},
[fetchOptions, options],
)
// Async yükleme (debounced)
const fetchAsync = useDebounce(async (query: string) => {
if (!fetchOptions) return
setLoading(true)
try {
const result = await fetchOptions(query)
setFilteredOptions(result)
} finally {
setLoading(false)
}
}, debounceMs)
const openDropdown = useCallback(
(query: string) => {
if (query.length < minChars && fetchOptions) return
setOpen(true)
setActiveIndex(-1)
if (fetchOptions) {
fetchAsync(query)
} else {
filterStatic(query)
}
},
[minChars, fetchOptions, fetchAsync, filterStatic],
)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value
if (!isControlled) setInputValue(val)
field?.onChange?.(val)
onInputChange?.(val)
openDropdown(val)
}
const handleFocus = () => {
openDropdown(inputValue)
}
const commitSelection = useCallback(
(option: AutoCompleteOption) => {
if (!isControlled) setInputValue(option.label)
field?.onChange?.(option.label)
onInputChange?.(option.label)
onSelect?.(option)
setOpen(false)
setActiveIndex(-1)
},
[isControlled, field, onInputChange, onSelect],
)
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (!open) {
if (e.key === 'ArrowDown') openDropdown(inputValue)
return
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setActiveIndex((i) =>
Math.min(i + 1, filteredOptions.length - 1),
)
break
case 'ArrowUp':
e.preventDefault()
setActiveIndex((i) => Math.max(i - 1, -1))
break
case 'Enter':
e.preventDefault()
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
commitSelection(filteredOptions[activeIndex])
}
break
case 'Escape':
setOpen(false)
setActiveIndex(-1)
break
case 'Tab':
setOpen(false)
break
}
}
// Aktif öğeyi listede görünür yap
useEffect(() => {
if (activeIndex < 0 || !listRef.current) return
const item = listRef.current.children[activeIndex] as HTMLElement
item?.scrollIntoView?.({ block: 'nearest' })
}, [activeIndex])
const handleClear = () => {
if (!isControlled) setInputValue('')
field?.onChange?.('')
onInputChange?.('')
setOpen(false)
setFilteredOptions([])
}
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
const invalidClass = 'border-red-500 focus:ring-red-500'
return (
<div
ref={containerRef}
className={classNames('autocomplete', className)}
style={style}
>
<div className="autocomplete-input-wrapper">
<input
ref={ref}
name={name ?? field?.name}
value={inputValue}
disabled={disabled}
placeholder={placeholder}
autoComplete="off"
className={classNames(
'autocomplete-input',
sizeClass,
invalid ? invalidClass : ringClass,
disabled && 'autocomplete-input-disabled',
clearable &&
inputValue &&
'autocomplete-input-clearable',
)}
role="combobox"
aria-autocomplete="list"
aria-expanded={open}
aria-controls={listboxId}
aria-activedescendant={
activeIndex >= 0
? `autocomplete-opt-${activeIndex}`
: undefined
}
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
{...rest}
/>
<div className="autocomplete-suffix">
{loading && (
<Spinner
size={14}
className="autocomplete-spinner"
/>
)}
{clearable && inputValue && !loading && (
<button
type="button"
className="autocomplete-clear"
onClick={handleClear}
tabIndex={-1}
aria-label="Temizle"
>
<svg
width={12}
height={12}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
)}
</div>
</div>
{open && (
<ul
ref={listRef}
id={listboxId}
role="listbox"
className="autocomplete-dropdown"
>
{loading ? (
<li className="autocomplete-option autocomplete-option-info">
{loadingText}
</li>
) : filteredOptions.length === 0 ? (
<li className="autocomplete-option autocomplete-option-info">
{noOptionsText}
</li>
) : (
filteredOptions.map((option, index) => (
<li
key={option.value}
id={`autocomplete-opt-${index}`}
role="option"
aria-selected={activeIndex === index}
aria-disabled={option.disabled}
className={classNames(
'autocomplete-option',
activeIndex === index &&
'autocomplete-option-active',
option.disabled &&
'autocomplete-option-disabled',
)}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (!option.disabled)
commitSelection(option)
}}
onMouseEnter={() => setActiveIndex(index)}
>
{renderOption
? renderOption(option, activeIndex === index)
: option.label}
</li>
))
)}
</ul>
)}
</div>
)
}, },
) [fetchOptions, options],
)
// Async yükleme (debounced)
const fetchAsync = useDebounce(async (query: string) => {
if (!fetchOptions) return
setLoading(true)
try {
const result = await fetchOptions(query)
setFilteredOptions(result)
} finally {
setLoading(false)
}
}, debounceMs)
const openDropdown = useCallback(
(query: string) => {
if (query.length < minChars && fetchOptions) return
setOpen(true)
setActiveIndex(-1)
if (fetchOptions) {
fetchAsync(query)
} else {
filterStatic(query)
}
},
[minChars, fetchOptions, fetchAsync, filterStatic],
)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value
if (!isControlled) setInputValue(val)
field?.onChange?.(val)
onInputChange?.(val)
openDropdown(val)
}
const handleFocus = () => {
openDropdown(inputValue)
}
const commitSelection = useCallback(
(option: AutoCompleteOption) => {
if (!isControlled) setInputValue(option.label)
field?.onChange?.(option.label)
onInputChange?.(option.label)
onSelect?.(option)
setOpen(false)
setActiveIndex(-1)
},
[isControlled, field, onInputChange, onSelect],
)
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (!open) {
if (e.key === 'ArrowDown') openDropdown(inputValue)
return
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setActiveIndex((i) => Math.min(i + 1, filteredOptions.length - 1))
break
case 'ArrowUp':
e.preventDefault()
setActiveIndex((i) => Math.max(i - 1, -1))
break
case 'Enter':
e.preventDefault()
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
commitSelection(filteredOptions[activeIndex])
}
break
case 'Escape':
setOpen(false)
setActiveIndex(-1)
break
case 'Tab':
setOpen(false)
break
}
}
// Aktif öğeyi listede görünür yap
useEffect(() => {
if (activeIndex < 0 || !listRef.current) return
const item = listRef.current.children[activeIndex] as HTMLElement
item?.scrollIntoView?.({ block: 'nearest' })
}, [activeIndex])
const handleClear = () => {
if (!isControlled) setInputValue('')
field?.onChange?.('')
onInputChange?.('')
setOpen(false)
setFilteredOptions([])
}
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
const invalidClass = 'border-red-500 focus:ring-red-500'
return (
<div ref={containerRef} className={classNames('autocomplete', className)} style={style}>
<div className="autocomplete-input-wrapper">
<input
ref={ref}
name={name ?? field?.name}
value={inputValue}
disabled={disabled}
placeholder={placeholder}
autoComplete="off"
className={classNames(
'autocomplete-input',
sizeClass,
invalid ? invalidClass : ringClass,
disabled && 'autocomplete-input-disabled',
clearable && inputValue && 'autocomplete-input-clearable',
)}
role="combobox"
aria-autocomplete="list"
aria-expanded={open}
aria-controls={listboxId}
aria-activedescendant={activeIndex >= 0 ? `autocomplete-opt-${activeIndex}` : undefined}
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
{...rest}
/>
<div className="autocomplete-suffix">
{loading && <Spinner size={14} className="autocomplete-spinner" />}
{clearable && inputValue && !loading && (
<button
type="button"
className="autocomplete-clear"
onClick={handleClear}
tabIndex={-1}
aria-label={translate('::App.DeveloperKit.CrudEndpoints.Clear')}
>
<svg
width={12}
height={12}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
)}
</div>
</div>
{open && (
<ul ref={listRef} id={listboxId} role="listbox" className="autocomplete-dropdown">
{loading ? (
<li className="autocomplete-option autocomplete-option-info">
{loadingText ?? translate('::App.Loading')}
</li>
) : filteredOptions.length === 0 ? (
<li className="autocomplete-option autocomplete-option-info">
{noOptionsText ?? translate('::App.AutoComplete.NoOptions')}
</li>
) : (
filteredOptions.map((option, index) => (
<li
key={option.value}
id={`autocomplete-opt-${index}`}
role="option"
aria-selected={activeIndex === index}
aria-disabled={option.disabled}
className={classNames(
'autocomplete-option',
activeIndex === index && 'autocomplete-option-active',
option.disabled && 'autocomplete-option-disabled',
)}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (!option.disabled) commitSelection(option)
}}
onMouseEnter={() => setActiveIndex(index)}
>
{renderOption ? renderOption(option, activeIndex === index) : option.label}
</li>
))
)}
</ul>
)}
</div>
)
})
AutoComplete.displayName = 'AutoComplete' AutoComplete.displayName = 'AutoComplete'

View file

@ -1,178 +1,147 @@
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 */
href?: string href?: string
/** Özel link bileşeni (react-router Link vb.) */ /** Özel link bileşeni (react-router Link vb.) */
as?: React.ElementType as?: React.ElementType
/** Aktif (son) öğe mi? Otomatik set edilir */ /** Aktif (son) öğe mi? Otomatik set edilir */
active?: boolean active?: boolean
/** İkon (solda) */ /** İkon (solda) */
icon?: ReactNode icon?: ReactNode
/** Tıklama eventi */ /** Tıklama eventi */
onClick?: (e: React.MouseEvent) => void onClick?: (e: React.MouseEvent) => void
} }
export interface BreadcrumbProps extends CommonProps { export interface BreadcrumbProps extends CommonProps {
/** Ayraç. Varsayılan: '/' */ /** Ayraç. Varsayılan: '/' */
separator?: ReactNode separator?: ReactNode
/** Öğeler arası boşluk. Varsayılan: 8 */ /** Öğeler arası boşluk. Varsayılan: 8 */
gap?: number gap?: number
/** Son öğe hariç metni maxWidth ile kırp */ /** Son öğe hariç metni maxWidth ile kırp */
maxItems?: number maxItems?: number
/** Küçük boyut */ /** Küçük boyut */
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, style,
style, href,
href, as: Component,
as: Component, active = false,
active = false, icon,
icon, onClick,
onClick, ...rest
...rest } = props
} = props
const Tag = Component ?? (href ? 'a' : 'span') const Tag = Component ?? (href ? 'a' : 'span')
return ( return (
<li <li
ref={ref} ref={ref}
className={classNames( className={classNames('breadcrumb-item', active && 'breadcrumb-item-active', className)}
'breadcrumb-item', style={style}
active && 'breadcrumb-item-active', aria-current={active ? 'page' : undefined}
className, {...rest}
)} >
style={style} <Tag
aria-current={active ? 'page' : undefined} href={href}
{...rest} className={classNames(
> 'breadcrumb-link',
<Tag active ? 'breadcrumb-link-active' : 'breadcrumb-link-default',
href={href} onClick && !active && 'cursor-pointer',
className={classNames( )}
'breadcrumb-link', onClick={onClick}
active >
? 'breadcrumb-link-active' {icon && <span className="breadcrumb-icon">{icon}</span>}
: 'breadcrumb-link-default', {children}
onClick && !active && 'cursor-pointer', </Tag>
)} </li>
onClick={onClick} )
> })
{icon && (
<span className="breadcrumb-icon">{icon}</span>
)}
{children}
</Tag>
</li>
)
},
)
BreadcrumbItem.displayName = 'BreadcrumbItem' BreadcrumbItem.displayName = 'BreadcrumbItem'
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => { const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
const { const { translate } = useLocalization()
className, const {
children, className,
style, children,
separator = '/', style,
gap = 8, separator = '/',
maxItems, gap = 8,
size = 'md', maxItems,
...rest size = 'md',
} = props ...rest
} = props
const items = Children.toArray(children).filter(isValidElement) const items = Children.toArray(children).filter(isValidElement)
const total = items.length const total = items.length
let visibleItems = items let visibleItems = items
let collapsedCount = 0 let collapsedCount = 0
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,
})
return (
<span key={index} className="breadcrumb-entry" style={{ gap }}>
{index === 1 && collapsedCount > 0 && (
<>
<span
className="breadcrumb-separator"
aria-hidden
style={{ marginInline: gap }}
>
{separator}
</span>
<span
className="breadcrumb-collapsed"
title={`${collapsedCount} öğe gizlendi`}
>
...
</span>
</>
)}
{index > 0 && (
<span
className="breadcrumb-separator"
aria-hidden
style={{ marginInline: gap }}
>
{separator}
</span>
)}
{cloned}
</span>
)
}) })
return ( return (
<nav <span key={index} className="breadcrumb-entry" style={{ gap }}>
ref={ref} {index === 1 && collapsedCount > 0 && (
aria-label="breadcrumb" <>
className={classNames( <span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
'breadcrumb-nav', {separator}
`breadcrumb-${size}`, </span>
className, <span
)} className="breadcrumb-collapsed"
style={style} title={translate('::App.Breadcrumb.CollapsedCount', { count: collapsedCount })}
{...rest} >
> ...
<ol className="breadcrumb-list">{renderedItems}</ol> </span>
</nav> </>
)}
{index > 0 && (
<span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
{separator}
</span>
)}
{cloned}
</span>
) )
})
return (
<nav
ref={ref}
aria-label="breadcrumb"
className={classNames('breadcrumb-nav', `breadcrumb-${size}`, className)}
style={style}
{...rest}
>
<ol className="breadcrumb-list">{renderedItems}</ol>
</nav>
)
}) })
Breadcrumb.displayName = 'Breadcrumb' Breadcrumb.displayName = 'Breadcrumb'
const BreadcrumbWithItem = Breadcrumb as typeof Breadcrumb & { const BreadcrumbWithItem = Breadcrumb as typeof Breadcrumb & {
Item: typeof BreadcrumbItem Item: typeof BreadcrumbItem
} }
BreadcrumbWithItem.Item = BreadcrumbItem BreadcrumbWithItem.Item = BreadcrumbItem

View file

@ -1,11 +1,11 @@
import { import {
forwardRef, forwardRef,
useState, useState,
useRef, useRef,
useCallback, useCallback,
useEffect, useEffect,
type KeyboardEvent, type KeyboardEvent,
type ReactNode, type ReactNode,
} from 'react' } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { useConfig } from '../ConfigProvider' import { useConfig } from '../ConfigProvider'
@ -13,240 +13,250 @@ 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 */
value?: string[] value?: string[]
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: string[] defaultValue?: string[]
/** Placeholder */ /** Placeholder */
placeholder?: string placeholder?: string
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Geçersiz */ /** Geçersiz */
invalid?: boolean invalid?: boolean
/** Boyut */ /** Boyut */
size?: TypeAttributes.ControlSize size?: TypeAttributes.ControlSize
/** Maks chip sayısı. 0 = sınırsız */ /** Maks chip sayısı. 0 = sınırsız */
max?: number max?: number
/** Çift değere izin verme. Varsayılan: false */ /** Çift değere izin verme. Varsayılan: false */
allowDuplicate?: boolean allowDuplicate?: boolean
/** Enter'a ek ayraç tuşu. Örn: ',' */ /** Enter'a ek ayraç tuşu. Örn: ',' */
separator?: string separator?: string
/** Chip özelleştirme render */ /** Chip özelleştirme render */
itemTemplate?: (value: string) => ReactNode itemTemplate?: (value: string) => ReactNode
/** Değer değiştiğinde */ /** Değer değiştiğinde */
onChange?: (value: string[]) => void onChange?: (value: string[]) => void
/** Chip eklendiğinde */ /** Chip eklendiğinde */
onAdd?: (value: string) => void onAdd?: (value: string) => void
/** Chip kaldırıldığında */ /** Chip kaldırıldığında */
onRemove?: (value: string) => void onRemove?: (value: string) => void
/** Input adı */ /** Input adı */
name?: string name?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
field?: any field?: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
form?: any form?: any
} }
const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => { const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
const { const { translate } = useLocalization()
const {
className,
style,
value: valueProp,
defaultValue = [],
placeholder,
disabled = false,
invalid = false,
size,
max = 0,
allowDuplicate = false,
separator,
itemTemplate,
onChange,
onAdd,
onRemove,
name,
field,
form,
...rest
} = props
const isControlled = valueProp !== undefined
const [chips, setChips] = useState<string[]>(
field?.value ?? (isControlled ? valueProp! : defaultValue),
)
const [inputVal, setInputVal] = useState('')
const [focusedChip, setFocusedChip] = useState<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const { themeColor, primaryColorLevel, controlSize } = useConfig()
const formControlSize = useForm()?.size
const inputGroupSize = useInputGroup()?.size
const resolvedSize = size || inputGroupSize || formControlSize || controlSize
const minH = `min-h-${CONTROL_SIZES[resolvedSize]}`
useEffect(() => {
if (isControlled) setChips(valueProp!)
}, [isControlled, valueProp])
useEffect(() => {
if (field?.value !== undefined) setChips(field.value)
}, [field?.value])
const commit = useCallback(
(next: string[]) => {
if (!isControlled) setChips(next)
field?.onChange?.(next)
onChange?.(next)
},
[isControlled, field, onChange],
)
const addChip = useCallback(
(raw: string) => {
const val = raw.trim()
if (!val) return
if (!allowDuplicate && chips.includes(val)) return
if (max > 0 && chips.length >= max) return
const next = [...chips, val]
commit(next)
onAdd?.(val)
setInputVal('')
},
[chips, allowDuplicate, max, commit, onAdd],
)
const removeChip = useCallback(
(index: number) => {
const removed = chips[index]
const next = chips.filter((_, i) => i !== index)
commit(next)
onRemove?.(removed)
setFocusedChip(null)
inputRef.current?.focus()
},
[chips, commit, onRemove],
)
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const val = inputVal
if (e.key === 'Enter') {
e.preventDefault()
addChip(val)
return
}
if (separator && e.key === separator) {
e.preventDefault()
addChip(val)
return
}
if (e.key === 'Backspace' && !val && chips.length > 0) {
removeChip(chips.length - 1)
return
}
if (e.key === 'ArrowLeft' && !val) {
setFocusedChip(chips.length - 1)
return
}
}
const handleChipKeyDown = (e: KeyboardEvent<HTMLSpanElement>, index: number) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault()
removeChip(index)
return
}
if (e.key === 'ArrowLeft') {
e.preventDefault()
setFocusedChip(Math.max(0, index - 1))
return
}
if (e.key === 'ArrowRight') {
e.preventDefault()
if (index === chips.length - 1) {
setFocusedChip(null)
inputRef.current?.focus()
} else {
setFocusedChip(index + 1)
}
}
}
const isMaxReached = max > 0 && chips.length >= max
const ringClass = `focus-within:ring-${themeColor}-${primaryColorLevel}`
return (
<div
ref={ref}
className={classNames(
'chips',
minH,
ringClass,
invalid && 'chips-invalid',
disabled && 'chips-disabled',
className, className,
style, )}
value: valueProp, style={style}
defaultValue = [], onClick={() => inputRef.current?.focus()}
placeholder, {...rest}
disabled = false, >
invalid = false, {chips.map((chip, i) => (
size, <span
max = 0, key={i}
allowDuplicate = false, className="chips-item"
separator, tabIndex={0}
itemTemplate, role="option"
onChange, aria-selected
onAdd, aria-label={chip}
onRemove, onFocus={() => setFocusedChip(i)}
name, onBlur={() => setFocusedChip(null)}
field, onKeyDown={(e) => handleChipKeyDown(e, i)}
form, data-focused={focusedChip === i}
...rest
} = props
const isControlled = valueProp !== undefined
const [chips, setChips] = useState<string[]>(
field?.value ?? (isControlled ? valueProp! : defaultValue),
)
const [inputVal, setInputVal] = useState('')
const [focusedChip, setFocusedChip] = useState<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const { themeColor, primaryColorLevel, controlSize } = useConfig()
const formControlSize = useForm()?.size
const inputGroupSize = useInputGroup()?.size
const resolvedSize = size || inputGroupSize || formControlSize || controlSize
const minH = `min-h-${CONTROL_SIZES[resolvedSize]}`
useEffect(() => {
if (isControlled) setChips(valueProp!)
}, [isControlled, valueProp])
useEffect(() => {
if (field?.value !== undefined) setChips(field.value)
}, [field?.value])
const commit = useCallback(
(next: string[]) => {
if (!isControlled) setChips(next)
field?.onChange?.(next)
onChange?.(next)
},
[isControlled, field, onChange],
)
const addChip = useCallback(
(raw: string) => {
const val = raw.trim()
if (!val) return
if (!allowDuplicate && chips.includes(val)) return
if (max > 0 && chips.length >= max) return
const next = [...chips, val]
commit(next)
onAdd?.(val)
setInputVal('')
},
[chips, allowDuplicate, max, commit, onAdd],
)
const removeChip = useCallback(
(index: number) => {
const removed = chips[index]
const next = chips.filter((_, i) => i !== index)
commit(next)
onRemove?.(removed)
setFocusedChip(null)
inputRef.current?.focus()
},
[chips, commit, onRemove],
)
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const val = inputVal
if (e.key === 'Enter') {
e.preventDefault()
addChip(val)
return
}
if (separator && e.key === separator) {
e.preventDefault()
addChip(val)
return
}
if (e.key === 'Backspace' && !val && chips.length > 0) {
removeChip(chips.length - 1)
return
}
if (e.key === 'ArrowLeft' && !val) {
setFocusedChip(chips.length - 1)
return
}
}
const handleChipKeyDown = (e: KeyboardEvent<HTMLSpanElement>, index: number) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault()
removeChip(index)
return
}
if (e.key === 'ArrowLeft') {
e.preventDefault()
setFocusedChip(Math.max(0, index - 1))
return
}
if (e.key === 'ArrowRight') {
e.preventDefault()
if (index === chips.length - 1) {
setFocusedChip(null)
inputRef.current?.focus()
} else {
setFocusedChip(index + 1)
}
}
}
const isMaxReached = max > 0 && chips.length >= max
const ringClass = `focus-within:ring-${themeColor}-${primaryColorLevel}`
return (
<div
ref={ref}
className={classNames(
'chips',
minH,
ringClass,
invalid && 'chips-invalid',
disabled && 'chips-disabled',
className,
)}
style={style}
onClick={() => inputRef.current?.focus()}
{...rest}
> >
{chips.map((chip, i) => ( <span className="chips-item-label">{itemTemplate ? itemTemplate(chip) : chip}</span>
<span {!disabled && (
key={i} <button
className="chips-item" type="button"
tabIndex={0} className="chips-item-remove"
role="option" onClick={(e) => {
aria-selected e.stopPropagation()
aria-label={chip} removeChip(i)
onFocus={() => setFocusedChip(i)} }}
onBlur={() => setFocusedChip(null)} tabIndex={-1}
onKeyDown={(e) => handleChipKeyDown(e, i)} aria-label={translate('::App.Chips.RemoveChip', { chip })}
data-focused={focusedChip === i} >
> <svg
<span className="chips-item-label"> width={10}
{itemTemplate ? itemTemplate(chip) : chip} height={10}
</span> viewBox="0 0 24 24"
{!disabled && ( fill="none"
<button stroke="currentColor"
type="button" strokeWidth={2.5}
className="chips-item-remove" >
onClick={(e) => { e.stopPropagation(); removeChip(i) }} <line x1="18" y1="6" x2="6" y2="18" />
tabIndex={-1} <line x1="6" y1="6" x2="18" y2="18" />
aria-label={`${chip} kaldır`} </svg>
> </button>
<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" /> </span>
<line x1="6" y1="6" x2="18" y2="18" /> ))}
</svg>
</button>
)}
</span>
))}
{!isMaxReached && !disabled && ( {!isMaxReached && !disabled && (
<input <input
ref={inputRef} ref={inputRef}
name={name ?? field?.name} name={name ?? field?.name}
className="chips-input" className="chips-input"
value={inputVal} value={inputVal}
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={() => {
if (inputVal.trim()) addChip(inputVal) if (inputVal.trim()) addChip(inputVal)
}} }}
/> />
)} )}
</div> </div>
) )
}) })
Chips.displayName = 'Chips' Chips.displayName = 'Chips'

View file

@ -1,41 +1,39 @@
import React, { import React, {
forwardRef, forwardRef,
useState, useState,
useRef, useRef,
useCallback, useCallback,
useEffect, useEffect,
type ChangeEvent, type ChangeEvent,
} 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 ───────────────────────────────────────────
const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
const cleaned = hex.replace('#', '') const cleaned = hex.replace('#', '')
if (cleaned.length !== 6 && cleaned.length !== 3) return null if (cleaned.length !== 6 && cleaned.length !== 3) return null
const full = const full =
cleaned.length === 3 cleaned.length === 3
? cleaned ? cleaned
.split('') .split('')
.map((c) => c + c) .map((c) => c + c)
.join('') .join('')
: cleaned : cleaned
const num = parseInt(full, 16) const num = parseInt(full, 16)
return { return {
r: (num >> 16) & 255, r: (num >> 16) & 255,
g: (num >> 8) & 255, g: (num >> 8) & 255,
b: num & 255, b: num & 255,
} }
} }
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('')
)
} }
const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val) const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val)
@ -43,265 +41,253 @@ const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val)
// ── Tipler ────────────────────────────────────────────────────────────────── // ── Tipler ──────────────────────────────────────────────────────────────────
export interface ColorPickerProps extends CommonProps { export interface ColorPickerProps extends CommonProps {
/** Kontrollü değer (hex: '#rrggbb') */ /** Kontrollü değer (hex: '#rrggbb') */
value?: string value?: string
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: string defaultValue?: string
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Boyut */ /** Boyut */
size?: TypeAttributes.ControlSize size?: TypeAttributes.ControlSize
/** Önceden tanımlı renk paleti */ /** Önceden tanımlı renk paleti */
presets?: string[] presets?: string[]
/** Alpha (opacity) kanalı göster */ /** Alpha (opacity) kanalı göster */
showAlpha?: boolean showAlpha?: boolean
/** Hex input göster. Varsayılan: true */ /** Hex input göster. Varsayılan: true */
showInput?: boolean showInput?: boolean
/** RGB değerlerini ayrı göster */ /** RGB değerlerini ayrı göster */
showRgb?: boolean showRgb?: boolean
/** Değer değiştiğinde callback */ /** Değer değiştiğinde callback */
onChange?: (hex: string) => void onChange?: (hex: string) => void
/** Input adı (form entegrasyonu) */ /** Input adı (form entegrasyonu) */
name?: string name?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
field?: any field?: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
form?: any form?: any
} }
// ── Bileşen ───────────────────────────────────────────────────────────────── // ── Bileşen ─────────────────────────────────────────────────────────────────
const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => { const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
const { const { translate } = useLocalization()
className, const {
style, className,
value: valueProp, style,
defaultValue = '#6366f1', value: valueProp,
disabled = false, defaultValue = '#6366f1',
size = 'md', disabled = false,
presets, size = 'md',
showInput = true, presets,
showRgb = false, showInput = true,
onChange, showRgb = false,
name, onChange,
field, name,
form: _form, field,
...rest form: _form,
} = props ...rest
} = props
const isControlled = valueProp !== undefined const isControlled = valueProp !== undefined
const fieldValue = field?.value const fieldValue = field?.value
const resolveInitial = () => { const resolveInitial = () => {
if (fieldValue && isValidHex(fieldValue)) return fieldValue if (fieldValue && isValidHex(fieldValue)) return fieldValue
if (isControlled && valueProp && isValidHex(valueProp)) return valueProp! if (isControlled && valueProp && isValidHex(valueProp)) return valueProp!
if (isValidHex(defaultValue)) return defaultValue if (isValidHex(defaultValue)) return defaultValue
return '#6366f1' return '#6366f1'
}
const [internalHex, setInternalHex] = useState<string>(resolveInitial)
const [inputText, setInputText] = useState<string>(resolveInitial)
const [open, setOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const nativeRef = useRef<HTMLInputElement>(null)
const currentHex = (() => {
const src = isControlled ? valueProp! : (fieldValue ?? internalHex)
return isValidHex(src) ? src : internalHex
})()
// sync input text ile dış değer
useEffect(() => {
setInputText(currentHex)
}, [currentHex])
// Dışarı tıklayınca kapat
useEffect(() => {
if (!open) return
const handler = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setOpen(false)
}
} }
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])
const [internalHex, setInternalHex] = useState<string>(resolveInitial) const commit = useCallback(
const [inputText, setInputText] = useState<string>(resolveInitial) (hex: string) => {
const [open, setOpen] = useState(false) if (!isControlled) setInternalHex(hex)
const containerRef = useRef<HTMLDivElement>(null) field?.onChange?.(hex)
const nativeRef = useRef<HTMLInputElement>(null) onChange?.(hex)
},
[isControlled, onChange, field],
)
const currentHex = (() => { const handleNativeChange = (e: ChangeEvent<HTMLInputElement>) => {
const src = isControlled ? valueProp! : fieldValue ?? internalHex const hex = e.target.value
return isValidHex(src) ? src : internalHex setInputText(hex)
})() commit(hex)
}
// sync input text ile dış değer const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
useEffect(() => { const raw = e.target.value
setInputText(currentHex) setInputText(raw)
}, [currentHex]) const normalized = raw.startsWith('#') ? raw : `#${raw}`
if (isValidHex(normalized)) {
// Dışarı tıklayınca kapat commit(normalized)
useEffect(() => {
if (!open) return
const handler = (e: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false)
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])
const commit = useCallback(
(hex: string) => {
if (!isControlled) setInternalHex(hex)
field?.onChange?.(hex)
onChange?.(hex)
},
[isControlled, onChange, field],
)
const handleNativeChange = (e: ChangeEvent<HTMLInputElement>) => {
const hex = e.target.value
setInputText(hex)
commit(hex)
} }
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputBlur = () => {
const raw = e.target.value const normalized = inputText.startsWith('#') ? inputText : `#${inputText}`
setInputText(raw) if (!isValidHex(normalized)) {
const normalized = raw.startsWith('#') ? raw : `#${raw}` setInputText(currentHex)
if (isValidHex(normalized)) {
commit(normalized)
}
} }
}
const handleInputBlur = () => { const handlePresetClick = (color: string) => {
const normalized = inputText.startsWith('#') ? inputText : `#${inputText}` setInputText(color)
if (!isValidHex(normalized)) { commit(color)
setInputText(currentHex) }
}
}
const handlePresetClick = (color: string) => { const rgb = hexToRgb(currentHex)
setInputText(color)
commit(color)
}
const rgb = hexToRgb(currentHex) const handleRgbChange = (channel: 'r' | 'g' | 'b', val: string) => {
if (!rgb) return
const num = Math.min(255, Math.max(0, parseInt(val) || 0))
const updated = { ...rgb, [channel]: num }
const hex = rgbToHex(updated.r, updated.g, updated.b)
setInputText(hex)
commit(hex)
}
const handleRgbChange = (channel: 'r' | 'g' | 'b', val: string) => { const sizeClass = {
if (!rgb) return lg: 'color-picker-lg',
const num = Math.min(255, Math.max(0, parseInt(val) || 0)) md: 'color-picker-md',
const updated = { ...rgb, [channel]: num } sm: 'color-picker-sm',
const hex = rgbToHex(updated.r, updated.g, updated.b) xs: 'color-picker-xs',
setInputText(hex) }[size]
commit(hex)
}
const sizeClass = { return (
lg: 'color-picker-lg', <div
md: 'color-picker-md', ref={(node) => {
sm: 'color-picker-sm', ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
xs: 'color-picker-xs', if (typeof ref === 'function') ref(node)
}[size] else if (ref) ref.current = node
}}
className={classNames('color-picker', sizeClass, className)}
style={style}
{...rest}
>
{/* Hidden native color input (asıl değer kaynağı) */}
<input
ref={nativeRef}
type="color"
name={name ?? field?.name}
value={currentHex}
disabled={disabled}
className="color-picker-native"
tabIndex={-1}
aria-hidden
onChange={handleNativeChange}
/>
return ( {/* Tetikleyici swatch */}
<div <button
ref={(node) => { type="button"
;( className={classNames('color-picker-swatch', disabled && 'color-picker-disabled')}
containerRef as React.MutableRefObject<HTMLDivElement | null> style={{ backgroundColor: currentHex }}
).current = node disabled={disabled}
if (typeof ref === 'function') ref(node) aria-label={translate('::App.ColorPicker.PickColor')}
else if (ref) ref.current = node onClick={() => {
}} if (disabled) return
className={classNames('color-picker', sizeClass, className)} setOpen((o) => !o)
style={style} }}
{...rest} />
>
{/* Hidden native color input (asıl değer kaynağı) */} {/* Popup panel */}
{open && (
<div className="color-picker-panel">
{/* Native picker (geniş spektrum) */}
<div className="color-picker-spectrum-wrapper">
<input <input
ref={nativeRef} type="color"
type="color" value={currentHex}
name={name ?? field?.name} className="color-picker-spectrum"
value={currentHex} onChange={handleNativeChange}
disabled={disabled}
className="color-picker-native"
tabIndex={-1}
aria-hidden
onChange={handleNativeChange}
/> />
</div>
{/* Tetikleyici swatch */} {/* Preset renkler */}
<button {presets && presets.length > 0 && (
type="button" <div className="color-picker-presets">
className={classNames( {presets.map((color) => (
'color-picker-swatch', <button
disabled && 'color-picker-disabled', key={color}
)} type="button"
style={{ backgroundColor: currentHex }} className={classNames(
disabled={disabled} 'color-picker-preset-dot',
aria-label="Renk seç" currentHex.toLowerCase() === color.toLowerCase() &&
onClick={() => { 'color-picker-preset-dot-active',
if (disabled) return )}
setOpen((o) => !o) style={{ backgroundColor: color }}
}} title={color}
/> onClick={() => handlePresetClick(color)}
aria-label={color}
/>
))}
</div>
)}
{/* Popup panel */} {/* Hex input */}
{open && ( {showInput && (
<div className="color-picker-panel"> <div className="color-picker-input-row">
{/* Native picker (geniş spektrum) */} <span className="color-picker-input-label">HEX</span>
<div className="color-picker-spectrum-wrapper"> <input
<input className="color-picker-text-input"
type="color" value={inputText}
value={currentHex} maxLength={7}
className="color-picker-spectrum" onChange={handleInputChange}
onChange={handleNativeChange} onBlur={handleInputBlur}
/> spellCheck={false}
</div> />
</div>
)}
{/* Preset renkler */} {/* RGB inputs */}
{presets && presets.length > 0 && ( {showRgb && rgb && (
<div className="color-picker-presets"> <div className="color-picker-rgb-row">
{presets.map((color) => ( {(['r', 'g', 'b'] as const).map((ch) => (
<button <div key={ch} className="color-picker-rgb-field">
key={color} <input
type="button" className="color-picker-text-input"
className={classNames( type="number"
'color-picker-preset-dot', min={0}
currentHex.toLowerCase() === max={255}
color.toLowerCase() && value={rgb[ch]}
'color-picker-preset-dot-active', onChange={(e) => handleRgbChange(ch, e.target.value)}
)} />
style={{ backgroundColor: color }} <span className="color-picker-input-label">{ch.toUpperCase()}</span>
title={color}
onClick={() => handlePresetClick(color)}
aria-label={color}
/>
))}
</div>
)}
{/* Hex input */}
{showInput && (
<div className="color-picker-input-row">
<span className="color-picker-input-label">HEX</span>
<input
className="color-picker-text-input"
value={inputText}
maxLength={7}
onChange={handleInputChange}
onBlur={handleInputBlur}
spellCheck={false}
/>
</div>
)}
{/* RGB inputs */}
{showRgb && rgb && (
<div className="color-picker-rgb-row">
{(['r', 'g', 'b'] as const).map((ch) => (
<div key={ch} className="color-picker-rgb-field">
<input
className="color-picker-text-input"
type="number"
min={0}
max={255}
value={rgb[ch]}
onChange={(e) =>
handleRgbChange(ch, e.target.value)
}
/>
<span className="color-picker-input-label">
{ch.toUpperCase()}
</span>
</div>
))}
</div>
)}
</div> </div>
)} ))}
</div>
)}
</div> </div>
) )}
</div>
)
}) })
ColorPicker.displayName = 'ColorPicker' ColorPicker.displayName = 'ColorPicker'

View file

@ -1,500 +1,523 @@
import { import {
forwardRef, forwardRef,
useState, useState,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
type ReactNode, type ReactNode,
type MouseEvent, type MouseEvent,
type WheelEvent, type WheelEvent,
} from 'react' } from 'react'
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 ──────────────────────────────────────────────────────────────────
export interface ImageViewerImage { export interface ImageViewerImage {
src: string src: string
alt?: string alt?: string
caption?: string caption?: string
thumbnail?: string thumbnail?: string
} }
export interface ImageViewerProps extends CommonProps { export interface ImageViewerProps extends CommonProps {
/** Görüntülenecek resimler */ /** Görüntülenecek resimler */
images: ImageViewerImage[] images: ImageViewerImage[]
/** Açılış resmi index'i (kontrolsüz) */ /** Açılış resmi index'i (kontrolsüz) */
defaultIndex?: number defaultIndex?: number
/** Kontrollü açık/kapalı durumu */ /** Kontrollü açık/kapalı durumu */
open?: boolean open?: boolean
/** Kontrollü aktif index */ /** Kontrollü aktif index */
activeIndex?: number activeIndex?: number
/** Kapatma isteği */ /** Kapatma isteği */
onClose?: () => void onClose?: () => void
/** Index değişimi */ /** Index değişimi */
onIndexChange?: (index: number) => void onIndexChange?: (index: number) => void
/** Döngü. Varsayılan: true */ /** Döngü. Varsayılan: true */
loop?: boolean loop?: boolean
/** Toolbar'ı göster. Varsayılan: true */ /** Toolbar'ı göster. Varsayılan: true */
showToolbar?: boolean showToolbar?: boolean
/** Thumbnailleri göster. Varsayılan: true */ /** Thumbnailleri göster. Varsayılan: true */
showThumbnails?: boolean showThumbnails?: boolean
/** Özel toolbar başlık sağ alanı */ /** Özel toolbar başlık sağ alanı */
toolbarExtra?: ReactNode toolbarExtra?: ReactNode
/** Klavye navigasyonu. Varsayılan: true */ /** Klavye navigasyonu. Varsayılan: true */
keyboard?: boolean keyboard?: boolean
/** Zoom adımı. Varsayılan: 0.25 */ /** Zoom adımı. Varsayılan: 0.25 */
zoomStep?: number zoomStep?: number
/** Min zoom. Varsayılan: 0.5 */ /** Min zoom. Varsayılan: 0.5 */
minZoom?: number minZoom?: number
/** Max zoom. Varsayılan: 4 */ /** Max zoom. Varsayılan: 4 */
maxZoom?: number maxZoom?: number
} }
// ── İkonlar ────────────────────────────────────────────────────────────────── // ── İkonlar ──────────────────────────────────────────────────────────────────
const IconClose = () => ( const IconClose = () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<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>
) )
const IconPrev = () => ( const IconPrev = () => (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="15 18 9 12 15 6" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
) )
const IconNext = () => ( const IconNext = () => (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="9 18 15 12 9 6" /> <polyline points="9 18 15 12 9 6" />
</svg> </svg>
) )
const IconZoomIn = () => ( const IconZoomIn = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" /> <line x1="21" y1="21" x2="16.65" y2="16.65" />
<line x1="11" y1="8" x2="11" y2="14" /> <line x1="11" y1="8" x2="11" y2="14" />
<line x1="8" y1="11" x2="14" y2="11" /> <line x1="8" y1="11" x2="14" y2="11" />
</svg> </svg>
) )
const IconZoomOut = () => ( const IconZoomOut = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" /> <line x1="21" y1="21" x2="16.65" y2="16.65" />
<line x1="8" y1="11" x2="14" y2="11" /> <line x1="8" y1="11" x2="14" y2="11" />
</svg> </svg>
) )
const IconReset = () => ( const IconReset = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="1 4 1 10 7 10" /> <polyline points="1 4 1 10 7 10" />
<path d="M3.51 15a9 9 0 1 0 .49-4.95" /> <path d="M3.51 15a9 9 0 1 0 .49-4.95" />
</svg> </svg>
) )
const IconRotateCW = () => ( const IconRotateCW = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="23 4 23 10 17 10" /> <polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-.49-4.95" /> <path d="M20.49 15a9 9 0 1 1-.49-4.95" />
</svg> </svg>
) )
const IconRotateCCW = () => ( const IconRotateCCW = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="1 4 1 10 7 10" /> <polyline points="1 4 1 10 7 10" />
<path d="M3.51 15a9 9 0 1 0 .49-4.95" /> <path d="M3.51 15a9 9 0 1 0 .49-4.95" />
</svg> </svg>
) )
const IconDownload = () => ( const IconDownload = () => (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}> <svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" /> <polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" /> <line x1="12" y1="15" x2="12" y2="3" />
</svg> </svg>
) )
// ── Overlay ────────────────────────────────────────────────────────────────── // ── Overlay ──────────────────────────────────────────────────────────────────
const ImageViewerOverlay = ({ const ImageViewerOverlay = ({
images, images,
index, index,
loop, loop,
showToolbar, showToolbar,
showThumbnails, showThumbnails,
toolbarExtra, toolbarExtra,
zoomStep, zoomStep,
minZoom, minZoom,
maxZoom, maxZoom,
onClose, onClose,
onIndexChange, onIndexChange,
}: { }: {
images: ImageViewerImage[] images: ImageViewerImage[]
index: number index: number
loop: boolean loop: boolean
showToolbar: boolean showToolbar: boolean
showThumbnails: boolean showThumbnails: boolean
toolbarExtra?: ReactNode toolbarExtra?: ReactNode
zoomStep: number zoomStep: number
minZoom: number minZoom: number
maxZoom: number maxZoom: number
onClose: () => void onClose: () => void
onIndexChange: (i: number) => void onIndexChange: (i: number) => void
}) => { }) => {
const [zoom, setZoom] = useState(1) const { translate } = useLocalization()
const [rotation, setRotation] = useState(0) const [zoom, setZoom] = useState(1)
const [dragging, setDragging] = useState(false) const [rotation, setRotation] = useState(0)
const [offset, setOffset] = useState({ x: 0, y: 0 }) const [dragging, setDragging] = useState(false)
const dragStart = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null) const [offset, setOffset] = useState({ x: 0, y: 0 })
const imgRef = useRef<HTMLImageElement>(null) const dragStart = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null)
const imgRef = useRef<HTMLImageElement>(null)
const current = images[index] const current = images[index]
const hasPrev = loop ? images.length > 1 : index > 0 const hasPrev = loop ? images.length > 1 : index > 0
const hasNext = loop ? images.length > 1 : index < images.length - 1 const hasNext = loop ? images.length > 1 : index < images.length - 1
const resetTransform = useCallback(() => { const resetTransform = useCallback(() => {
setZoom(1) setZoom(1)
setRotation(0) setRotation(0)
setOffset({ x: 0, y: 0 }) setOffset({ x: 0, y: 0 })
}, []) }, [])
// 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
onIndexChange(loop ? (index + 1) % images.length : index + 1) onIndexChange(loop ? (index + 1) % images.length : index + 1)
}, [hasNext, loop, index, images.length, onIndexChange]) }, [hasNext, loop, index, images.length, onIndexChange])
const goPrev = useCallback(() => { const goPrev = useCallback(() => {
if (!hasPrev) return if (!hasPrev) return
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 rotateCW = useCallback(() => setRotation((r) => r + 90), []) const zoomOut = useCallback(
const rotateCCW = useCallback(() => setRotation((r) => r - 90), []) () => setZoom((z) => Math.max(z - zoomStep, minZoom)),
[zoomStep, minZoom],
)
const rotateCW = useCallback(() => setRotation((r) => r + 90), [])
const rotateCCW = useCallback(() => setRotation((r) => r - 90), [])
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
const src = current.src const src = current.src
const isDataUri = src.startsWith('data:') const isDataUri = src.startsWith('data:')
if (isDataUri) { if (isDataUri) {
// data:[<mime>];base64,<data> veya data:[<mime>],<data> // data:[<mime>];base64,<data> veya data:[<mime>],<data>
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(/\.[^.]+$/, '') const a = document.createElement('a')
: 'image' a.href = src
const a = document.createElement('a') a.download = `${baseName}.${ext}`
a.href = src a.click()
a.download = `${baseName}.${ext}` } else {
a.click() // Normal URL - dosya adını URL'den çıkar, yoksa alt veya 'image' kullan
} else { let fileName = current.alt ?? ''
// Normal URL - dosya adını URL'den çıkar, yoksa alt veya 'image' kullan if (!fileName) {
let fileName = current.alt ?? '' try {
if (!fileName) { const url = new URL(src, window.location.href)
try { const pathParts = url.pathname.split('/')
const url = new URL(src, window.location.href) fileName = pathParts[pathParts.length - 1] || 'image'
const pathParts = url.pathname.split('/') } catch {
fileName = pathParts[pathParts.length - 1] || 'image' fileName = 'image'
} catch {
fileName = 'image'
}
}
const a = document.createElement('a')
a.href = src
a.download = fileName
// Aynı origin değilse fetch + blob ile zorla indir
try {
const isSameOrigin =
new URL(src, window.location.href).origin === window.location.origin
if (!isSameOrigin) {
fetch(src)
.then((r) => r.blob())
.then((blob) => {
const blobUrl = URL.createObjectURL(blob)
a.href = blobUrl
a.click()
URL.revokeObjectURL(blobUrl)
})
return
}
} catch { /* origin parse hatası — direkt dene */ }
a.click()
} }
}, [current]) }
const a = document.createElement('a')
// Keyboard a.href = src
useEffect(() => { a.download = fileName
const handler = (e: KeyboardEvent) => { // Aynı origin değilse fetch + blob ile zorla indir
switch (e.key) { try {
case 'ArrowLeft': goPrev(); break const isSameOrigin = new URL(src, window.location.href).origin === window.location.origin
case 'ArrowRight': goNext(); break if (!isSameOrigin) {
case 'Escape': onClose(); break fetch(src)
case '+': case '=': zoomIn(); break .then((r) => r.blob())
case '-': zoomOut(); break .then((blob) => {
case 'r': rotateCW(); break const blobUrl = URL.createObjectURL(blob)
case 'R': rotateCCW(); break a.href = blobUrl
} a.click()
URL.revokeObjectURL(blobUrl)
})
return
} }
window.addEventListener('keydown', handler) } catch {
return () => window.removeEventListener('keydown', handler) /* origin parse hatası — direkt dene */
}, [goPrev, goNext, onClose, zoomIn, zoomOut, rotateCW, rotateCCW]) }
a.click()
// Wheel zoom
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
e.preventDefault()
if (e.deltaY < 0) zoomIn()
else zoomOut()
} }
}, [current])
// Drag (pan) // Keyboard
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => { useEffect(() => {
if (zoom <= 1) return const handler = (e: KeyboardEvent) => {
e.preventDefault() switch (e.key) {
setDragging(true) case 'ArrowLeft':
dragStart.current = { x: e.clientX, y: e.clientY, ox: offset.x, oy: offset.y } goPrev()
break
case 'ArrowRight':
goNext()
break
case 'Escape':
onClose()
break
case '+':
case '=':
zoomIn()
break
case '-':
zoomOut()
break
case 'r':
rotateCW()
break
case 'R':
rotateCCW()
break
}
} }
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => { window.addEventListener('keydown', handler)
if (!dragging || !dragStart.current) return return () => window.removeEventListener('keydown', handler)
setOffset({ }, [goPrev, goNext, onClose, zoomIn, zoomOut, rotateCW, rotateCCW])
x: dragStart.current.ox + (e.clientX - dragStart.current.x),
y: dragStart.current.oy + (e.clientY - dragStart.current.y),
})
}
const handleMouseUp = () => { setDragging(false); dragStart.current = null }
const overlayClick = (e: MouseEvent<HTMLDivElement>) => { // Wheel zoom
if (e.target === e.currentTarget) onClose() const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
} e.preventDefault()
if (e.deltaY < 0) zoomIn()
else zoomOut()
}
return ( // Drag (pan)
<div const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
className="image-viewer-overlay" if (zoom <= 1) return
role="dialog" e.preventDefault()
aria-modal setDragging(true)
aria-label={current.alt ?? 'Resim görüntüleyici'} dragStart.current = { x: e.clientX, y: e.clientY, ox: offset.x, oy: offset.y }
> }
{/* Toolbar */} const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
{showToolbar && ( if (!dragging || !dragStart.current) return
<div className="image-viewer-toolbar"> setOffset({
<span className="image-viewer-counter"> x: dragStart.current.ox + (e.clientX - dragStart.current.x),
{index + 1} / {images.length} y: dragStart.current.oy + (e.clientY - dragStart.current.y),
</span> })
{current.caption && ( }
<span className="image-viewer-caption">{current.caption}</span> const handleMouseUp = () => {
)} setDragging(false)
<div className="image-viewer-toolbar-actions"> dragStart.current = null
{toolbarExtra} }
<button type="button" onClick={zoomOut} title="Uzaklaştır (-)">
<IconZoomOut />
</button>
<span className="image-viewer-zoom-label">
{Math.round(zoom * 100)}%
</span>
<button type="button" onClick={zoomIn} title="Yaklaştır (+)">
<IconZoomIn />
</button>
<button type="button" onClick={resetTransform} title="Sıfırla">
<IconReset />
</button>
<button type="button" onClick={rotateCCW} title="Sola döndür (R)">
<IconRotateCCW />
</button>
<button type="button" onClick={rotateCW} title="Sağa döndür (r)">
<IconRotateCW />
</button>
<button type="button" onClick={handleDownload} title="İndir">
<IconDownload />
</button>
<button type="button" onClick={onClose} title="Kapat (Esc)" className="image-viewer-close">
<IconClose />
</button>
</div>
</div>
)}
{/* Stage */} const overlayClick = (e: MouseEvent<HTMLDivElement>) => {
<div if (e.target === e.currentTarget) onClose()
className={classNames( }
'image-viewer-stage',
dragging && 'image-viewer-dragging', return (
)} <div
onClick={overlayClick} className="image-viewer-overlay"
onWheel={handleWheel} role="dialog"
onMouseDown={handleMouseDown} aria-modal
onMouseMove={handleMouseMove} aria-label={current.alt ?? translate('::App.ImageViewer.Title')}
onMouseUp={handleMouseUp} >
onMouseLeave={handleMouseUp} {/* Toolbar */}
{showToolbar && (
<div className="image-viewer-toolbar">
<span className="image-viewer-counter">
{index + 1} / {images.length}
</span>
{current.caption && <span className="image-viewer-caption">{current.caption}</span>}
<div className="image-viewer-toolbar-actions">
{toolbarExtra}
<button type="button" onClick={zoomOut} title={translate('::App.Platform.ZoomOut')}>
<IconZoomOut />
</button>
<span className="image-viewer-zoom-label">{Math.round(zoom * 100)}%</span>
<button type="button" onClick={zoomIn} title={translate('::App.Platform.ZoomIn')}>
<IconZoomIn />
</button>
<button type="button" onClick={resetTransform} title={translate('::Reset')}>
<IconReset />
</button>
<button
type="button"
onClick={rotateCCW}
title={translate('::App.ImageViewer.RotateLeft')}
> >
<img <IconRotateCCW />
ref={imgRef} </button>
src={current.src} <button
alt={current.alt ?? ''} type="button"
className="image-viewer-img" onClick={rotateCW}
draggable={false} title={translate('::App.ImageViewer.RotateRight')}
style={{ >
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`, <IconRotateCW />
cursor: zoom > 1 ? (dragging ? 'grabbing' : 'grab') : 'default', </button>
}} <button type="button" onClick={handleDownload} title={translate('::Download')}>
/> <IconDownload />
</div> </button>
<button
{/* Prev / Next */} type="button"
{hasPrev && ( onClick={onClose}
<button title={translate('::App.Platform.CloseWithEsc')}
type="button" className="image-viewer-close"
className="image-viewer-nav image-viewer-nav-prev" >
onClick={goPrev} <IconClose />
aria-label="Önceki" </button>
> </div>
<IconPrev />
</button>
)}
{hasNext && (
<button
type="button"
className="image-viewer-nav image-viewer-nav-next"
onClick={goNext}
aria-label="Sonraki"
>
<IconNext />
</button>
)}
{/* Thumbnails */}
{showThumbnails && images.length > 1 && (
<div className="image-viewer-thumbnails">
{images.map((img, i) => (
<button
key={i}
type="button"
onClick={() => onIndexChange(i)}
className={classNames(
'image-viewer-thumb',
i === index && 'image-viewer-thumb-active',
)}
aria-label={img.alt ?? `Resim ${i + 1}`}
aria-pressed={i === index}
>
<img
src={img.thumbnail ?? img.src}
alt={img.alt ?? ''}
draggable={false}
/>
</button>
))}
</div>
)}
</div> </div>
) )}
{/* Stage */}
<div
className={classNames('image-viewer-stage', dragging && 'image-viewer-dragging')}
onClick={overlayClick}
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<img
ref={imgRef}
src={current.src}
alt={current.alt ?? ''}
className="image-viewer-img"
draggable={false}
style={{
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
cursor: zoom > 1 ? (dragging ? 'grabbing' : 'grab') : 'default',
}}
/>
</div>
{/* Prev / Next */}
{hasPrev && (
<button
type="button"
className="image-viewer-nav image-viewer-nav-prev"
onClick={goPrev}
aria-label={translate('::App.VisualDesigner.Canvas.Previous')}
>
<IconPrev />
</button>
)}
{hasNext && (
<button
type="button"
className="image-viewer-nav image-viewer-nav-next"
onClick={goNext}
aria-label={translate('::App.VisualDesigner.Canvas.Next')}
>
<IconNext />
</button>
)}
{/* Thumbnails */}
{showThumbnails && images.length > 1 && (
<div className="image-viewer-thumbnails">
{images.map((img, i) => (
<button
key={i}
type="button"
onClick={() => onIndexChange(i)}
className={classNames(
'image-viewer-thumb',
i === index && 'image-viewer-thumb-active',
)}
aria-label={img.alt ?? translate('::App.ImageViewer.ImageN', { index: i + 1 })}
aria-pressed={i === index}
>
<img src={img.thumbnail ?? img.src} alt={img.alt ?? ''} draggable={false} />
</button>
))}
</div>
)}
</div>
)
} }
// ── Ana Komponent ──────────────────────────────────────────────────────────── // ── Ana Komponent ────────────────────────────────────────────────────────────
const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => { const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => {
const { const { translate } = useLocalization()
className, const {
style, className,
images, style,
defaultIndex = 0, images,
open: openProp, defaultIndex = 0,
activeIndex: activeIndexProp, open: openProp,
onClose, activeIndex: activeIndexProp,
onIndexChange, onClose,
loop = true, onIndexChange,
showToolbar = true, loop = true,
showThumbnails = true, showToolbar = true,
toolbarExtra, showThumbnails = true,
keyboard: _keyboard = true, toolbarExtra,
zoomStep = 0.25, keyboard: _keyboard = true,
minZoom = 0.5, zoomStep = 0.25,
maxZoom = 4, minZoom = 0.5,
children, maxZoom = 4,
...rest children,
} = props ...rest
} = props
const isControlled = openProp !== undefined const isControlled = openProp !== undefined
const [internalOpen, setInternalOpen] = useState(false) const [internalOpen, setInternalOpen] = useState(false)
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)
onClose?.() onClose?.()
}, [isControlled, onClose]) }, [isControlled, onClose])
const handleIndexChange = useCallback( const handleIndexChange = useCallback(
(i: number) => { (i: number) => {
if (activeIndexProp === undefined) setInternalIndex(i) if (activeIndexProp === undefined) setInternalIndex(i)
onIndexChange?.(i) onIndexChange?.(i)
}, },
[activeIndexProp, onIndexChange], [activeIndexProp, onIndexChange],
) )
const openAt = useCallback( const openAt = useCallback(
(index: number) => { (index: number) => {
setInternalIndex(index) setInternalIndex(index)
if (!isControlled) setInternalOpen(true) if (!isControlled) setInternalOpen(true)
}, },
[isControlled], [isControlled],
) )
if (!images || images.length === 0) return null if (!images || images.length === 0) return null
return ( return (
<> <>
{/* Trigger wrapper (children varsa tıklanabilir galeri) */} {/* Trigger wrapper (children varsa tıklanabilir galeri) */}
{children && ( {children && (
<div <div
ref={ref} ref={ref}
className={classNames('image-viewer-trigger', className)} className={classNames('image-viewer-trigger', className)}
style={style} style={style}
{...rest} {...rest}
> >
{images.map((img, i) => ( {images.map((img, i) => (
<button <button
key={i} key={i}
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} </button>
alt={img.alt ?? ''} ))}
draggable={false} </div>
/> )}
</button>
))}
</div>
)}
{/* Portal overlay */} {/* Portal overlay */}
{isOpen && {isOpen &&
createPortal( createPortal(
<ImageViewerOverlay <ImageViewerOverlay
images={images} images={images}
index={currentIndex} index={currentIndex}
loop={loop} loop={loop}
showToolbar={showToolbar} showToolbar={showToolbar}
showThumbnails={showThumbnails} showThumbnails={showThumbnails}
toolbarExtra={toolbarExtra} toolbarExtra={toolbarExtra}
zoomStep={zoomStep} zoomStep={zoomStep}
minZoom={minZoom} minZoom={minZoom}
maxZoom={maxZoom} maxZoom={maxZoom}
onClose={handleClose} onClose={handleClose}
onIndexChange={handleIndexChange} onIndexChange={handleIndexChange}
/>, />,
document.body, document.body,
)} )}
</> </>
) )
}) })
ImageViewer.displayName = 'ImageViewer' ImageViewer.displayName = 'ImageViewer'

View file

@ -1,327 +1,316 @@
import React, { import React, {
forwardRef, forwardRef,
useState, useState,
useRef, useRef,
useCallback, useCallback,
useEffect, useEffect,
type KeyboardEvent, type KeyboardEvent,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
} from 'react' } from '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'
export interface KnobProps extends CommonProps { export interface KnobProps extends CommonProps {
/** Kontrollü değer */ /** Kontrollü değer */
value?: number value?: number
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: number defaultValue?: number
/** Minimum değer. Varsayılan: 0 */ /** Minimum değer. Varsayılan: 0 */
min?: number min?: number
/** Maksimum değer. Varsayılan: 100 */ /** Maksimum değer. Varsayılan: 100 */
max?: number max?: number
/** Adım büyüklüğü. Varsayılan: 1 */ /** Adım büyüklüğü. Varsayılan: 1 */
step?: number step?: number
/** Çap (px). Varsayılan: 100 */ /** Çap (px). Varsayılan: 100 */
size?: number size?: number
/** Çizgi kalınlığı (px). Varsayılan: 14 */ /** Çizgi kalınlığı (px). Varsayılan: 14 */
strokeWidth?: number strokeWidth?: number
/** Değer yayının rengi. Varsayılan: tema rengi */ /** Değer yayının rengi. Varsayılan: tema rengi */
valueColor?: string valueColor?: string
/** Arka plan yayının rengi */ /** Arka plan yayının rengi */
rangeColor?: string rangeColor?: string
/** Etiket metni rengi */ /** Etiket metni rengi */
textColor?: string textColor?: string
/** Değer şablonu. '{value}' placeholder'ı. Varsayılan: '{value}' */ /** Değer şablonu. '{value}' placeholder'ı. Varsayılan: '{value}' */
valueTemplate?: string valueTemplate?: string
/** Salt okunur */ /** Salt okunur */
readOnly?: boolean readOnly?: boolean
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Etiket göster. Varsayılan: true */ /** Etiket göster. Varsayılan: true */
showValue?: boolean showValue?: boolean
/** Değer değiştiğinde */ /** Değer değiştiğinde */
onChange?: (value: number) => void onChange?: (value: number) => void
/** Input adı (hidden) */ /** Input adı (hidden) */
name?: string name?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
field?: any field?: any
} }
const KNOB_START_ANGLE = -220 // derece (saat 7 hizası) 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 {
className, className,
style, style,
value: valueProp, value: valueProp,
defaultValue = 0, defaultValue = 0,
min = 0, min = 0,
max = 100, max = 100,
step = 1, step = 1,
size = 100, size = 100,
strokeWidth = 14, strokeWidth = 14,
valueColor, valueColor,
rangeColor, rangeColor,
textColor, textColor,
valueTemplate = '{value}', valueTemplate = '{value}',
readOnly = false, readOnly = false,
disabled = false, disabled = false,
showValue = true, showValue = true,
onChange, onChange,
name, name,
field, field,
...rest ...rest
} = props } = props
const { themeColor, primaryColorLevel } = useConfig() const { themeColor, primaryColorLevel } = useConfig()
const isControlled = valueProp !== undefined const isControlled = valueProp !== undefined
const [internalValue, setInternalValue] = useState( const [internalValue, setInternalValue] = useState(
clamp(field?.value ?? (isControlled ? valueProp! : defaultValue), min, max), clamp(field?.value ?? (isControlled ? valueProp! : defaultValue), min, max),
) )
useEffect(() => { useEffect(() => {
if (isControlled) setInternalValue(clamp(valueProp!, min, max)) if (isControlled) setInternalValue(clamp(valueProp!, min, max))
}, [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) => {
const snapped = clamp(snap(raw, step, min), min, max) const snapped = clamp(snap(raw, step, min), min, max)
if (!isControlled) setInternalValue(snapped) if (!isControlled) setInternalValue(snapped)
field?.onChange?.(snapped) field?.onChange?.(snapped)
onChange?.(snapped) onChange?.(snapped)
}, },
[isControlled, step, min, max, field, onChange], [isControlled, step, min, max, field, onChange],
) )
// SVG geometri // SVG geometri
const r = (size - strokeWidth) / 2 const r = (size - strokeWidth) / 2
const cx = size / 2 const cx = size / 2
const cy = size / 2 const cy = size / 2
const circumference = 2 * Math.PI * r const circumference = 2 * Math.PI * r
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
// SVG default: 0 derece = sağ (3 saat), CW pozitif // SVG default: 0 derece = sağ (3 saat), CW pozitif
// Bizim 0 = üst → rotate(-90) + açı offset // Bizim 0 = üst → rotate(-90) + açı offset
const startAngleRad = ((KNOB_START_ANGLE - 90) * Math.PI) / 180 const startAngleRad = ((KNOB_START_ANGLE - 90) * Math.PI) / 180
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))
// Pointer/drag etkileşimi // Pointer/drag etkileşimi
const svgRef = useRef<SVGSVGElement>(null) const svgRef = useRef<SVGSVGElement>(null)
const dragging = useRef(false) const dragging = useRef(false)
const angleToValue = useCallback( const angleToValue = useCallback(
(angleDeg: number) => { (angleDeg: number) => {
// Normalize to [KNOB_START_ANGLE, KNOB_END_ANGLE] // Normalize to [KNOB_START_ANGLE, KNOB_END_ANGLE]
let a = angleDeg - KNOB_START_ANGLE let a = angleDeg - KNOB_START_ANGLE
if (a < 0) a = 0 if (a < 0) a = 0
if (a > KNOB_RANGE) a = KNOB_RANGE if (a > KNOB_RANGE) a = KNOB_RANGE
return min + (a / KNOB_RANGE) * (max - min) return min + (a / KNOB_RANGE) * (max - min)
}, },
[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() const centerX = rect.left + rect.width / 2
const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2
const centerY = rect.top + rect.height / 2 const dx = clientX - centerX
const dx = clientX - centerX const dy = clientY - centerY
const dy = clientY - centerY // atan2 → derece, SVG 0=sağ → 90 ekle için 90 çıkarıyoruz
// atan2 → derece, SVG 0=sağ → 90 ekle için 90 çıkarıyoruz let angle = (Math.atan2(dy, dx) * 180) / Math.PI + 90
let angle = (Math.atan2(dy, dx) * 180) / Math.PI + 90 // Normalize: KNOB_START_ANGLE -220 → üst-sol
// Normalize: KNOB_START_ANGLE -220 → üst-sol if (angle < KNOB_START_ANGLE + 360) {
if (angle < KNOB_START_ANGLE + 360) { // Döngü düzeltmesi
// Döngü düzeltmesi
}
return angle
},
[],
)
const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => {
if (disabled || readOnly) return
e.currentTarget.setPointerCapture(e.pointerId)
dragging.current = true
const angle = getAngleFromPointer(e.clientX, e.clientY)
commit(angleToValue(angle))
} }
return angle
}, [])
const handlePointerMove = (e: ReactPointerEvent<SVGSVGElement>) => { const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => {
if (!dragging.current || disabled || readOnly) return if (disabled || readOnly) return
const angle = getAngleFromPointer(e.clientX, e.clientY) e.currentTarget.setPointerCapture(e.pointerId)
commit(angleToValue(angle)) dragging.current = true
const angle = getAngleFromPointer(e.clientX, e.clientY)
commit(angleToValue(angle))
}
const handlePointerMove = (e: ReactPointerEvent<SVGSVGElement>) => {
if (!dragging.current || disabled || readOnly) return
const angle = getAngleFromPointer(e.clientX, e.clientY)
commit(angleToValue(angle))
}
const handlePointerUp = () => {
dragging.current = false
}
const handleKeyDown = (e: KeyboardEvent<SVGSVGElement>) => {
if (disabled || readOnly) return
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault()
commit(currentValue + step)
break
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault()
commit(currentValue - step)
break
case 'Home':
e.preventDefault()
commit(min)
break
case 'End':
e.preventDefault()
commit(max)
break
case 'PageUp':
e.preventDefault()
commit(currentValue + step * 10)
break
case 'PageDown':
e.preventDefault()
commit(currentValue - step * 10)
break
} }
}
const handlePointerUp = () => { // Stroke-dasharray/offset hesaplama
dragging.current = false // Toplam çevre üzerinde KNOB_RANGE kadar gösteriyoruz
} const dashArray = `${fullArcLength} ${circumference}`
const valueDash = `${valueArcLength} ${circumference}`
const handleKeyDown = (e: KeyboardEvent<SVGSVGElement>) => { // Rotation: SVG'de 0 derece sağ → başlangıç açısını ayarlamak için rotate uygulayacağız
if (disabled || readOnly) return const trackRotation = `rotate(${KNOB_START_ANGLE + 90}, ${cx}, ${cy})`
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault()
commit(currentValue + step)
break
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault()
commit(currentValue - step)
break
case 'Home':
e.preventDefault()
commit(min)
break
case 'End':
e.preventDefault()
commit(max)
break
case 'PageUp':
e.preventDefault()
commit(currentValue + step * 10)
break
case 'PageDown':
e.preventDefault()
commit(currentValue - step * 10)
break
}
}
// Stroke-dasharray/offset hesaplama return (
// Toplam çevre üzerinde KNOB_RANGE kadar gösteriyoruz <span className={classNames('knob-wrapper', className)} style={style}>
const dashArray = `${fullArcLength} ${circumference}` {name && <input type="hidden" name={name ?? field?.name} value={currentValue} />}
const valueDash = `${valueArcLength} ${circumference}` <svg
ref={(node) => {
// Rotation: SVG'de 0 derece sağ → başlangıç açısını ayarlamak için rotate uygulayacağız ;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node
const trackRotation = `rotate(${KNOB_START_ANGLE + 90}, ${cx}, ${cy})` if (typeof ref === 'function') ref(node)
else if (ref) ref.current = node
return ( }}
<span className={classNames('knob-wrapper', className)} style={style}> width={size}
{name && ( height={size}
<input type="hidden" name={name ?? field?.name} value={currentValue} /> viewBox={`0 0 ${size} ${size}`}
)} className={classNames(
<svg 'knob',
ref={(node) => { !disabled && !readOnly && 'knob-interactive',
;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node disabled && 'knob-disabled',
if (typeof ref === 'function') ref(node) )}
else if (ref) ref.current = node role="slider"
}} aria-valuemin={min}
width={size} aria-valuemax={max}
height={size} aria-valuenow={currentValue}
viewBox={`0 0 ${size} ${size}`} aria-valuetext={label}
className={classNames( aria-disabled={disabled}
'knob', aria-readonly={readOnly}
!disabled && !readOnly && 'knob-interactive', tabIndex={disabled || readOnly ? -1 : 0}
disabled && 'knob-disabled', onPointerDown={handlePointerDown}
)} onPointerMove={handlePointerMove}
role="slider" onPointerUp={handlePointerUp}
aria-valuemin={min} onKeyDown={handleKeyDown}
aria-valuemax={max} {...rest}
aria-valuenow={currentValue} >
aria-valuetext={label} {/* Arka plan yayı */}
aria-disabled={disabled} <circle
aria-readonly={readOnly} cx={cx}
tabIndex={disabled || readOnly ? -1 : 0} cy={cy}
onPointerDown={handlePointerDown} r={r}
onPointerMove={handlePointerMove} fill="none"
onPointerUp={handlePointerUp} stroke={resolveRangeColor()}
onKeyDown={handleKeyDown} strokeWidth={strokeWidth}
{...rest} strokeLinecap="round"
> strokeDasharray={dashArray}
{/* Arka plan yayı */} transform={trackRotation}
<circle className="knob-range"
cx={cx} />
cy={cy} {/* Değer yayı */}
r={r} {valueArcLength > 0 && (
fill="none" <circle
stroke={resolveRangeColor()} cx={cx}
strokeWidth={strokeWidth} cy={cy}
strokeLinecap="round" r={r}
strokeDasharray={dashArray} fill="none"
transform={trackRotation} stroke={resolveValueColor()}
className="knob-range" strokeWidth={strokeWidth}
/> strokeLinecap="round"
{/* Değer yayı */} strokeDasharray={valueDash}
{valueArcLength > 0 && ( transform={trackRotation}
<circle className="knob-value-arc"
cx={cx} />
cy={cy} )}
r={r} {/* Handle noktası */}
fill="none" {(() => {
stroke={resolveValueColor()} const handleAngle = (valueAngleDeg - 90) * (Math.PI / 180)
strokeWidth={strokeWidth} const hx = cx + r * Math.cos(handleAngle)
strokeLinecap="round" const hy = cy + r * Math.sin(handleAngle)
strokeDasharray={valueDash} return (
transform={trackRotation} <circle
className="knob-value-arc" cx={hx}
/> cy={hy}
)} r={strokeWidth / 2 + 1}
{/* Handle noktası */} fill={resolveValueColor()}
{(() => { className="knob-handle"
const handleAngle = (valueAngleDeg - 90) * (Math.PI / 180) />
const hx = cx + r * Math.cos(handleAngle) )
const hy = cy + r * Math.sin(handleAngle) })()}
return ( {/* Etiket */}
<circle {showValue && (
cx={hx} <text
cy={hy} x={cx}
r={strokeWidth / 2 + 1} y={cy}
fill={resolveValueColor()} textAnchor="middle"
className="knob-handle" dominantBaseline="central"
/> fontSize={size * 0.2}
) fontWeight={600}
})()} fill={resolveTextColor()}
{/* Etiket */} className="knob-label"
{showValue && ( >
<text {label}
x={cx} </text>
y={cy} )}
textAnchor="middle" </svg>
dominantBaseline="central" </span>
fontSize={size * 0.2} )
fontWeight={600}
fill={resolveTextColor()}
className="knob-label"
>
{label}
</text>
)}
</svg>
</span>
)
}) })
Knob.displayName = 'Knob' Knob.displayName = 'Knob'

View file

@ -1,219 +1,201 @@
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'
export interface MarqueeProps extends CommonProps { export interface MarqueeProps extends CommonProps {
/** Oynatma hızı (piksel/saniye). Varsayılan: 50 */ /** Oynatma hızı (piksel/saniye). Varsayılan: 50 */
speed?: number speed?: number
/** Kaydırma yönü. Varsayılan: 'left' */ /** Kaydırma yönü. Varsayılan: 'left' */
direction?: 'left' | 'right' | 'up' | 'down' direction?: 'left' | 'right' | 'up' | 'down'
/** Fare üzerindeyken duraklat. Varsayılan: false */ /** Fare üzerindeyken duraklat. Varsayılan: false */
pauseOnHover?: boolean pauseOnHover?: boolean
/** Tıklandığında duraklat. Varsayılan: false */ /** Tıklandığında duraklat. Varsayılan: false */
pauseOnClick?: boolean pauseOnClick?: boolean
/** Döngü sayısı. 0 = sonsuz. Varsayılan: 0 */ /** Döngü sayısı. 0 = sonsuz. Varsayılan: 0 */
loop?: number loop?: number
/** Kenar degradesi göster. Varsayılan: true */ /** Kenar degradesi göster. Varsayılan: true */
gradient?: boolean gradient?: boolean
/** Degrade rengi (rgb). Varsayılan: '255,255,255' */ /** Degrade rengi (rgb). Varsayılan: '255,255,255' */
gradientColor?: string gradientColor?: string
/** Degrade genişliği. Varsayılan: 200 */ /** Degrade genişliği. Varsayılan: 200 */
gradientWidth?: number | string gradientWidth?: number | string
/** Başlangıç gecikmesi (ms). Varsayılan: 0 */ /** Başlangıç gecikmesi (ms). Varsayılan: 0 */
delay?: number delay?: number
/** Oynatma durumu. Varsayılan: true */ /** Oynatma durumu. Varsayılan: true */
play?: boolean play?: boolean
/** İçeriği otomatik doldur. Varsayılan: false */ /** İçeriği otomatik doldur. Varsayılan: false */
autoFill?: boolean autoFill?: boolean
/** Animasyon tamamlandığında callback */ /** Animasyon tamamlandığında callback */
onFinish?: () => void onFinish?: () => void
/** Döngü tamamlandığında callback */ /** Döngü tamamlandığında callback */
onCycleComplete?: () => void onCycleComplete?: () => void
/** Oynatma başladığında callback */ /** Oynatma başladığında callback */
onMount?: () => void onMount?: () => void
} }
const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => { const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
const { const {
className, className,
children, children,
style, style,
speed = 50, speed = 50,
direction = 'left', direction = 'left',
pauseOnHover = false, pauseOnHover = false,
pauseOnClick = false, pauseOnClick = false,
loop = 0, loop = 0,
gradient = true, gradient = true,
gradientColor = '255,255,255', gradientColor = '255,255,255',
gradientWidth = 200, gradientWidth = 200,
delay = 0, delay = 0,
play = true, play = true,
autoFill = false, autoFill = false,
onFinish, onFinish,
onCycleComplete, onCycleComplete,
onMount, onMount,
...rest ...rest
} = props } = props
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const trackRef = useRef<HTMLDivElement>(null) const trackRef = useRef<HTMLDivElement>(null)
const [trackWidth, setTrackWidth] = useState(0) const [trackWidth, setTrackWidth] = useState(0)
const [containerWidth, setContainerWidth] = useState(0) const [containerWidth, setContainerWidth] = useState(0)
const [multiplier, setMultiplier] = useState(1) const [multiplier, setMultiplier] = useState(1)
const [isMounted, setIsMounted] = useState(false) const [isMounted, setIsMounted] = useState(false)
const isVertical = direction === 'up' || direction === 'down' const isVertical = direction === 'up' || direction === 'down'
const isReverse = direction === 'right' || direction === 'down' const isReverse = direction === 'right' || direction === 'down'
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))
} else { } else {
setMultiplier(1) setMultiplier(1)
} }
setTrackWidth(tw) setTrackWidth(tw)
setContainerWidth(cw) setContainerWidth(cw)
}
}, [autoFill, isVertical])
useEffect(() => {
calculateWidth()
const resizeObserver = new ResizeObserver(calculateWidth)
if (containerRef.current) resizeObserver.observe(containerRef.current)
if (trackRef.current) resizeObserver.observe(trackRef.current)
return () => resizeObserver.disconnect()
}, [calculateWidth, children])
useEffect(() => {
setIsMounted(true)
onMount?.()
}, [onMount])
const duration =
trackWidth > 0
? autoFill
? (trackWidth * multiplier) / speed
: Math.max(trackWidth, containerWidth) / speed
: 0
const gradientStyle: CSSProperties = gradient
? {
['--marquee-gradient-color' as string]: `rgba(${gradientColor}, 1), rgba(${gradientColor}, 0)`,
}
: {}
const containerStyle: CSSProperties = {
...gradientStyle,
...style,
['--marquee-gradient-width' as string]:
typeof gradientWidth === 'number' ? `${gradientWidth}px` : gradientWidth,
}
const animationStyle: CSSProperties = {
['--marquee-duration' as string]: `${duration}s`,
['--marquee-delay' as string]: `${delay}s`,
['--marquee-iteration-count' as string]: loop > 0 ? `${loop}` : 'infinite',
}
const animationClass = isVertical
? isReverse
? 'marquee-animate-down'
: 'marquee-animate-up'
: isReverse
? 'marquee-animate-right'
: 'marquee-animate-left'
const isPaused = !play
const handleAnimationIteration = () => {
onCycleComplete?.()
}
const handleAnimationEnd = () => {
onFinish?.()
}
const clonedItems = Array.from({ length: multiplier }, (_, i) => (
<Fragment key={i}>{children}</Fragment>
))
return (
<div
ref={(node) => {
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
if (typeof ref === 'function') {
ref(node)
} else if (ref) {
ref.current = node
} }
}, [autoFill, isVertical]) }}
className={classNames(
useEffect(() => { 'marquee-container',
calculateWidth() isVertical && 'marquee-vertical',
const resizeObserver = new ResizeObserver(calculateWidth) gradient && 'marquee-gradient',
if (containerRef.current) resizeObserver.observe(containerRef.current) className,
if (trackRef.current) resizeObserver.observe(trackRef.current) )}
return () => resizeObserver.disconnect() style={containerStyle}
}, [calculateWidth, children]) {...rest}
>
useEffect(() => { {isMounted && (
setIsMounted(true) <>
onMount?.() <div
}, [onMount]) ref={trackRef}
const duration =
trackWidth > 0
? autoFill
? (trackWidth * multiplier) / speed
: Math.max(trackWidth, containerWidth) / speed
: 0
const gradientStyle: CSSProperties = gradient
? {
['--marquee-gradient-color' as string]: `rgba(${gradientColor}, 1), rgba(${gradientColor}, 0)`,
}
: {}
const containerStyle: CSSProperties = {
...gradientStyle,
...style,
['--marquee-gradient-width' as string]:
typeof gradientWidth === 'number'
? `${gradientWidth}px`
: gradientWidth,
}
const animationStyle: CSSProperties = {
['--marquee-duration' as string]: `${duration}s`,
['--marquee-delay' as string]: `${delay}s`,
['--marquee-iteration-count' as string]: loop > 0 ? `${loop}` : 'infinite',
}
const animationClass = isVertical
? isReverse
? 'marquee-animate-down'
: 'marquee-animate-up'
: isReverse
? 'marquee-animate-right'
: 'marquee-animate-left'
const isPaused = !play
const handleAnimationIteration = () => {
onCycleComplete?.()
}
const handleAnimationEnd = () => {
onFinish?.()
}
const clonedItems = Array.from({ length: multiplier }, (_, i) => (
<Fragment key={i}>{children}</Fragment>
))
return (
<div
ref={(node) => {
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current =
node
if (typeof ref === 'function') {
ref(node)
} else if (ref) {
ref.current = node
}
}}
className={classNames( className={classNames(
'marquee-container', 'marquee-track',
isVertical && 'marquee-vertical', animationClass,
gradient && 'marquee-gradient', (pauseOnHover || isPaused) && 'marquee-pause-on-hover',
className, (pauseOnClick || isPaused) && isPaused && 'marquee-paused',
)} )}
style={containerStyle} style={animationStyle}
{...rest} onAnimationIteration={handleAnimationIteration}
> onAnimationEnd={handleAnimationEnd}
{isMounted && ( aria-hidden={multiplier > 1}
<> >
<div {clonedItems}
ref={trackRef} </div>
className={classNames( {/* Klonlanmış track - kesintisiz görünüm için */}
'marquee-track', <div
animationClass, className={classNames(
(pauseOnHover || isPaused) && 'marquee-track',
'marquee-pause-on-hover', animationClass,
(pauseOnClick || isPaused) && (pauseOnHover || isPaused) && 'marquee-pause-on-hover',
isPaused && isPaused && 'marquee-paused',
'marquee-paused',
)}
style={animationStyle}
onAnimationIteration={handleAnimationIteration}
onAnimationEnd={handleAnimationEnd}
aria-hidden={multiplier > 1}
>
{clonedItems}
</div>
{/* Klonlanmış track - kesintisiz görünüm için */}
<div
className={classNames(
'marquee-track',
animationClass,
(pauseOnHover || isPaused) &&
'marquee-pause-on-hover',
isPaused && 'marquee-paused',
)}
style={animationStyle}
aria-hidden
>
{clonedItems}
</div>
</>
)} )}
</div> style={animationStyle}
) aria-hidden
>
{clonedItems}
</div>
</>
)}
</div>
)
}) })
Marquee.displayName = 'Marquee' Marquee.displayName = 'Marquee'

View file

@ -7,109 +7,84 @@ import type { CommonProps, TypeAttributes } from '../@types/common'
import type { ReactNode, MouseEvent } from 'react' import type { ReactNode, MouseEvent } from 'react'
export interface NotificationProps extends CommonProps { export interface NotificationProps extends CommonProps {
closable?: boolean closable?: boolean
customIcon?: ReactNode | string customIcon?: ReactNode | string
duration?: number duration?: number
onClose?: (e: MouseEvent<HTMLSpanElement>) => void onClose?: (e: MouseEvent<HTMLSpanElement>) => void
title?: string title?: string
triggerByToast?: boolean triggerByToast?: boolean
type?: TypeAttributes.Status type?: TypeAttributes.Status
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, closable = false,
closable = false, customIcon,
customIcon, duration = 3000,
duration = 3000, onClose,
onClose, style,
style, title,
title, triggerByToast,
triggerByToast, type,
type, width = 350,
width = 350, ...rest
...rest } = props
} = props
// Toast tarafından yönetilmeyen bildirimler kapatma animasyonu // Toast tarafından yönetilmeyen bildirimler kapatma animasyonu
// 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>) => {
onClose?.(e) onClose?.(e)
clear() clear()
if (!triggerByToast) { if (!triggerByToast) {
setTimeout(() => { setTimeout(() => {
setHidden(true) setHidden(true)
}, 400) }, 400)
} }
}, },
[onClose, clear, triggerByToast] [onClose, clear, triggerByToast],
) )
const notificationClass = classNames('notification', className) const notificationClass = classNames('notification', className)
if (hidden) { if (hidden) {
return null return null
} }
return ( return (
<div <div ref={ref} {...rest} className={notificationClass} style={{ width: width, ...style }}>
ref={ref} <div className={classNames('notification-content', !children && 'no-child')}>
{...rest} {type && !customIcon ? (
className={notificationClass} <div className="mr-3">
style={{ width: width, ...style }} <StatusIcon type={type} />
> </div>
<div ) : null}
className={classNames( {customIcon && <div className="mr-3">{customIcon}</div>}
'notification-content', <div className="mr-4">
!children && 'no-child' {title && (
)} <div className={classNames('notification-title', !!children && 'mb-1')}>{title}</div>
> )}
{type && !customIcon ? ( <div className="notification-description">{children}</div>
<div className="mr-3"> </div>
<StatusIcon type={type} /> </div>
</div> {closable && (
) : null} <CloseButton
{customIcon && <div className="mr-3">{customIcon}</div>} className="notification-close"
<div className="mr-4"> defaultStyle={false}
{title && ( absolute={true}
<div onClick={handleClose}
className={classNames( />
'notification-title', )}
!!children && 'mb-1' </div>
)} )
> })
{title}
</div>
)}
<div className="notification-description">
{children}
</div>
</div>
</div>
{closable && (
<CloseButton
className="notification-close"
defaultStyle={false}
absolute={true}
onClick={handleClose}
/>
)}
</div>
)
}
)
Notification.displayName = 'Notification' Notification.displayName = 'Notification'

View file

@ -1,229 +1,223 @@
import React, { import React, {
forwardRef, forwardRef,
useState, useState,
useCallback, useCallback,
useRef, useRef,
type KeyboardEvent, type KeyboardEvent,
type ReactNode, type ReactNode,
} from 'react' } from '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 */
count?: number count?: number
/** Kontrollü değer */ /** Kontrollü değer */
value?: number value?: number
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: number defaultValue?: number
/** Yarım yıldız desteği. Varsayılan: false */ /** Yarım yıldız desteği. Varsayılan: false */
allowHalf?: boolean allowHalf?: boolean
/** Temizlemeye izin ver (aynı yıldıza tıkla = sıfırla). Varsayılan: true */ /** Temizlemeye izin ver (aynı yıldıza tıkla = sıfırla). Varsayılan: true */
allowClear?: boolean allowClear?: boolean
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Salt okunur */ /** Salt okunur */
readOnly?: boolean readOnly?: boolean
/** Özel ikon (dolu, boş) */ /** Özel ikon (dolu, boş) */
character?: ReactNode | ((index: number) => ReactNode) character?: ReactNode | ((index: number) => ReactNode)
/** Tema rengi. Varsayılan: 'amber' */ /** Tema rengi. Varsayılan: 'amber' */
color?: string color?: string
/** Yıldız boyutu (px). Varsayılan: 20 */ /** Yıldız boyutu (px). Varsayılan: 20 */
size?: number size?: number
/** Boşluk arası (px). Varsayılan: 4 */ /** Boşluk arası (px). Varsayılan: 4 */
gap?: number gap?: number
/** Değer değiştiğinde callback */ /** Değer değiştiğinde callback */
onChange?: (value: number) => void onChange?: (value: number) => void
/** Hover değiştiğinde callback */ /** Hover değiştiğinde callback */
onHoverChange?: (value: number) => void onHoverChange?: (value: number) => void
/** Tooltip metinleri */ /** Tooltip metinleri */
tooltips?: string[] tooltips?: string[]
} }
const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size: number }) => ( const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size: number }) => (
<svg <svg
width={size} width={size}
height={size} height={size}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
{half && ( {half && (
<defs> <defs>
<linearGradient id="rate-half"> <linearGradient id="rate-half">
<stop offset="50%" stopColor="currentColor" /> <stop offset="50%" stopColor="currentColor" />
<stop offset="50%" stopColor="transparent" /> <stop offset="50%" stopColor="transparent" />
</linearGradient> </linearGradient>
</defs> </defs>
)} )}
<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 stroke="currentColor"
? 'url(#rate-half)' strokeWidth="1.5"
: filled strokeLinecap="round"
? 'currentColor' strokeLinejoin="round"
: 'none' />
} </svg>
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) )
const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => { const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
const { const { translate } = useLocalization()
className, const {
style, className,
count = 5, style,
value: valueProp, count = 5,
defaultValue = 0, value: valueProp,
allowHalf = false, defaultValue = 0,
allowClear = true, allowHalf = false,
disabled = false, allowClear = true,
readOnly = false, disabled = false,
character, readOnly = false,
color = 'amber', character,
size = 20, color = 'amber',
gap = 4, size = 20,
onChange, gap = 4,
onHoverChange, onChange,
tooltips, onHoverChange,
...rest tooltips,
} = props ...rest
} = props
const { themeColor, primaryColorLevel } = useConfig() const { themeColor, primaryColorLevel } = useConfig()
const isControlled = valueProp !== undefined const isControlled = valueProp !== undefined
const [internalValue, setInternalValue] = useState(defaultValue) const [internalValue, setInternalValue] = useState(defaultValue)
const [hoverValue, setHoverValue] = useState<number | null>(null) const [hoverValue, setHoverValue] = useState<number | null>(null)
const lastClickedRef = useRef<number | null>(null) const lastClickedRef = useRef<number | null>(null)
const value = isControlled ? valueProp! : internalValue const value = isControlled ? valueProp! : internalValue
const displayValue = hoverValue !== null ? hoverValue : value const displayValue = hoverValue !== null ? hoverValue : value
const resolveColor = () => { const resolveColor = () => {
if (color === 'theme') return `text-${themeColor}-${primaryColorLevel}` if (color === 'theme') return `text-${themeColor}-${primaryColorLevel}`
return `text-${color}-400` return `text-${color}-400`
}
const getStarValue = (index: number, offsetX: number, starWidth: number) => {
if (allowHalf && offsetX < starWidth / 2) {
return index + 0.5
} }
return index + 1
}
const getStarValue = (index: number, offsetX: number, starWidth: number) => { const handleMouseMove = useCallback(
if (allowHalf && offsetX < starWidth / 2) { (index: number, e: React.MouseEvent<HTMLSpanElement>) => {
return index + 0.5 if (disabled || readOnly) return
} const rect = e.currentTarget.getBoundingClientRect()
return index + 1 const offsetX = e.clientX - rect.left
} const newVal = getStarValue(index, offsetX, rect.width)
if (newVal !== hoverValue) {
setHoverValue(newVal)
onHoverChange?.(newVal)
}
},
[disabled, readOnly, hoverValue, allowHalf, onHoverChange],
)
const handleMouseMove = useCallback( const handleMouseLeave = useCallback(() => {
(index: number, e: React.MouseEvent<HTMLSpanElement>) => { if (disabled || readOnly) return
if (disabled || readOnly) return setHoverValue(null)
const rect = e.currentTarget.getBoundingClientRect() onHoverChange?.(0)
const offsetX = e.clientX - rect.left }, [disabled, readOnly, onHoverChange])
const newVal = getStarValue(index, offsetX, rect.width)
if (newVal !== hoverValue) {
setHoverValue(newVal)
onHoverChange?.(newVal)
}
},
[disabled, readOnly, hoverValue, allowHalf, onHoverChange],
)
const handleMouseLeave = useCallback(() => { const handleClick = useCallback(
if (disabled || readOnly) return (index: number, e: React.MouseEvent<HTMLSpanElement>) => {
setHoverValue(null) if (disabled || readOnly) return
onHoverChange?.(0) const rect = e.currentTarget.getBoundingClientRect()
}, [disabled, readOnly, onHoverChange]) const offsetX = e.clientX - rect.left
const clickedVal = getStarValue(index, offsetX, rect.width)
const handleClick = useCallback( let newVal = clickedVal
(index: number, e: React.MouseEvent<HTMLSpanElement>) => { if (allowClear && clickedVal === lastClickedRef.current) {
if (disabled || readOnly) return newVal = 0
const rect = e.currentTarget.getBoundingClientRect() lastClickedRef.current = null
const offsetX = e.clientX - rect.left } else {
const clickedVal = getStarValue(index, offsetX, rect.width) lastClickedRef.current = clickedVal
}
let newVal = clickedVal if (!isControlled) setInternalValue(newVal)
if (allowClear && clickedVal === lastClickedRef.current) { onChange?.(newVal)
newVal = 0 },
lastClickedRef.current = null [disabled, readOnly, allowClear, allowHalf, isControlled, onChange],
} else { )
lastClickedRef.current = clickedVal
}
if (!isControlled) setInternalValue(newVal) const handleKeyDown = useCallback(
onChange?.(newVal) (e: KeyboardEvent<HTMLDivElement>) => {
}, if (disabled || readOnly) return
[disabled, readOnly, allowClear, allowHalf, isControlled, onChange], let newVal = value
) if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
newVal = Math.min(value + (allowHalf ? 0.5 : 1), count)
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
newVal = Math.max(value - (allowHalf ? 0.5 : 1), 0)
} else {
return
}
e.preventDefault()
if (!isControlled) setInternalValue(newVal)
onChange?.(newVal)
},
[disabled, readOnly, value, allowHalf, count, isControlled, onChange],
)
const handleKeyDown = useCallback( const renderStar = (index: number) => {
(e: KeyboardEvent<HTMLDivElement>) => { const filled = displayValue >= index + 1
if (disabled || readOnly) return const half = !filled && allowHalf && displayValue >= index + 0.5
let newVal = value const tooltip = tooltips?.[index]
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
newVal = Math.min(value + (allowHalf ? 0.5 : 1), count)
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
newVal = Math.max(value - (allowHalf ? 0.5 : 1), 0)
} else {
return
}
e.preventDefault()
if (!isControlled) setInternalValue(newVal)
onChange?.(newVal)
},
[disabled, readOnly, value, allowHalf, count, isControlled, onChange],
)
const renderStar = (index: number) => { const starNode =
const filled = displayValue >= index + 1 typeof character === 'function'
const half = !filled && allowHalf && displayValue >= index + 0.5 ? character(index)
const tooltip = tooltips?.[index] : (character ?? <StarIcon filled={filled} half={half} size={size} />)
const starNode =
typeof character === 'function'
? character(index)
: character ?? (
<StarIcon filled={filled} half={half} size={size} />
)
return (
<span
key={index}
className={classNames(
'rate-star',
filled || half ? resolveColor() : 'text-gray-300 dark:text-gray-600',
!disabled && !readOnly && 'cursor-pointer',
disabled && 'opacity-50 cursor-not-allowed',
)}
style={{ marginRight: index < count - 1 ? gap : 0 }}
title={tooltip}
onMouseMove={(e) => handleMouseMove(index, e)}
onClick={(e) => handleClick(index, e)}
role={!disabled && !readOnly ? 'radio' : undefined}
aria-checked={displayValue >= index + 1}
aria-label={tooltip ?? `${index + 1} yıldız`}
>
{starNode}
</span>
)
}
return ( return (
<div <span
ref={ref} key={index}
className={classNames('rate', className)} className={classNames(
style={style} 'rate-star',
onMouseLeave={handleMouseLeave} filled || half ? resolveColor() : 'text-gray-300 dark:text-gray-600',
onKeyDown={handleKeyDown} !disabled && !readOnly && 'cursor-pointer',
tabIndex={disabled || readOnly ? undefined : 0} disabled && 'opacity-50 cursor-not-allowed',
role="radiogroup" )}
aria-label="Değerlendirme" style={{ marginRight: index < count - 1 ? gap : 0 }}
{...rest} title={tooltip}
> onMouseMove={(e) => handleMouseMove(index, e)}
{Array.from({ length: count }, (_, i) => renderStar(i))} onClick={(e) => handleClick(index, e)}
</div> role={!disabled && !readOnly ? 'radio' : undefined}
aria-checked={displayValue >= index + 1}
aria-label={tooltip ?? translate('::App.Rate.StarCount', { count: index + 1 })}
>
{starNode}
</span>
) )
}
return (
<div
ref={ref}
className={classNames('rate', className)}
style={style}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
tabIndex={disabled || readOnly ? undefined : 0}
role="radiogroup"
aria-label={translate('::App.Rate.Label')}
{...rest}
>
{Array.from({ length: count }, (_, i) => renderStar(i))}
</div>
)
}) })
Rate.displayName = 'Rate' Rate.displayName = 'Rate'

View file

@ -1,11 +1,11 @@
import React, { import React, {
forwardRef, forwardRef,
useState, useState,
useRef, useRef,
useCallback, useCallback,
useEffect, useEffect,
type KeyboardEvent, type KeyboardEvent,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
} from 'react' } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { useConfig } from '../ConfigProvider' import { useConfig } from '../ConfigProvider'
@ -14,366 +14,365 @@ import type { CommonProps } from '../@types/common'
export type SliderValue = number | [number, number] export type SliderValue = number | [number, number]
export interface SliderProps extends CommonProps { export interface SliderProps extends CommonProps {
/** Kontrollü değer (tek veya range) */ /** Kontrollü değer (tek veya range) */
value?: SliderValue value?: SliderValue
/** Başlangıç değeri (kontrolsüz) */ /** Başlangıç değeri (kontrolsüz) */
defaultValue?: SliderValue defaultValue?: SliderValue
/** Minimum değer. Varsayılan: 0 */ /** Minimum değer. Varsayılan: 0 */
min?: number min?: number
/** Maksimum değer. Varsayılan: 100 */ /** Maksimum değer. Varsayılan: 100 */
max?: number max?: number
/** Adım büyüklüğü. Varsayılan: 1 */ /** Adım büyüklüğü. Varsayılan: 1 */
step?: number step?: number
/** Range modu (iki handle). Varsayılan: false */ /** Range modu (iki handle). Varsayılan: false */
range?: boolean range?: boolean
/** Yön. Varsayılan: 'horizontal' */ /** Yön. Varsayılan: 'horizontal' */
orientation?: 'horizontal' | 'vertical' orientation?: 'horizontal' | 'vertical'
/** Track yüksekliği/genişliği (px). Varsayılan: 4 */ /** Track yüksekliği/genişliği (px). Varsayılan: 4 */
trackSize?: number trackSize?: number
/** Handle çapı (px). Varsayılan: 18 */ /** Handle çapı (px). Varsayılan: 18 */
handleSize?: number handleSize?: number
/** Devre dışı */ /** Devre dışı */
disabled?: boolean disabled?: boolean
/** Salt okunur */ /** Salt okunur */
readOnly?: boolean readOnly?: boolean
/** Tooltip göster */ /** Tooltip göster */
tooltip?: boolean tooltip?: boolean
/** Adım işaretleri göster */ /** Adım işaretleri göster */
marks?: boolean | { value: number; label?: string }[] marks?: boolean | { value: number; label?: string }[]
/** Değer değiştiğinde (sürükleme dahil) */ /** Değer değiştiğinde (sürükleme dahil) */
onChange?: (value: SliderValue) => void onChange?: (value: SliderValue) => void
/** Sadece bırakıldığında */ /** Sadece bırakıldığında */
onAfterChange?: (value: SliderValue) => void onAfterChange?: (value: SliderValue) => void
/** Input adı */ /** Input adı */
name?: string name?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
field?: any field?: any
} }
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 {
className, className,
style, style,
value: valueProp, value: valueProp,
defaultValue, defaultValue,
min = 0, min = 0,
max = 100, max = 100,
step = 1, step = 1,
range = false, range = false,
orientation = 'horizontal', orientation = 'horizontal',
trackSize = 4, trackSize = 4,
handleSize = 18, handleSize = 18,
disabled = false, disabled = false,
readOnly = false, readOnly = false,
tooltip = false, tooltip = false,
marks, marks,
onChange, onChange,
onAfterChange, onAfterChange,
name, name,
field, field,
...rest ...rest
} = props } = props
const isVertical = orientation === 'vertical' const isVertical = orientation === 'vertical'
const isControlled = valueProp !== undefined const isControlled = valueProp !== undefined
const resolveDefault = (): SliderValue => { const resolveDefault = (): SliderValue => {
if (field?.value !== undefined) return field.value if (field?.value !== undefined) return field.value
if (isControlled) return valueProp! if (isControlled) return valueProp!
if (defaultValue !== undefined) return defaultValue if (defaultValue !== undefined) return defaultValue
return range ? [min, max] : min return range ? [min, max] : min
} }
const [internalValue, setInternalValue] = useState<SliderValue>(resolveDefault) const [internalValue, setInternalValue] = useState<SliderValue>(resolveDefault)
const activeHandle = useRef<0 | 1>(0) const activeHandle = useRef<0 | 1>(0)
const trackRef = useRef<HTMLDivElement>(null) const trackRef = useRef<HTMLDivElement>(null)
const dragging = useRef(false) const dragging = useRef(false)
const [showTooltip, setShowTooltip] = useState<boolean[]>([false, false]) const [showTooltip, setShowTooltip] = useState<boolean[]>([false, false])
useEffect(() => { useEffect(() => {
if (isControlled) setInternalValue(valueProp!) if (isControlled) setInternalValue(valueProp!)
}, [isControlled, valueProp]) }, [isControlled, valueProp])
useEffect(() => { useEffect(() => {
if (field?.value !== undefined) setInternalValue(field.value) if (field?.value !== undefined) setInternalValue(field.value)
}, [field?.value]) }, [field?.value])
const { themeColor, primaryColorLevel } = useConfig() const { themeColor, primaryColorLevel } = useConfig()
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) => {
if (!isControlled) setInternalValue(next) if (!isControlled) setInternalValue(next)
field?.onChange?.(next) field?.onChange?.(next)
onChange?.(next) onChange?.(next)
if (final) onAfterChange?.(next) if (final) onAfterChange?.(next)
}, },
[isControlled, field, onChange, onAfterChange], [isControlled, field, onChange, onAfterChange],
) )
const percentOf = (v: number) => ((v - min) / (max - min)) * 100 const percentOf = (v: number) => ((v - min) / (max - min)) * 100
const valueFromPointer = useCallback( const valueFromPointer = useCallback(
(clientX: number, clientY: number): number => { (clientX: number, clientY: number): number => {
const track = trackRef.current const track = trackRef.current
if (!track) return min if (!track) return min
const rect = track.getBoundingClientRect() const rect = track.getBoundingClientRect()
let ratio: number let ratio: number
if (isVertical) { if (isVertical) {
ratio = 1 - (clientY - rect.top) / rect.height ratio = 1 - (clientY - rect.top) / rect.height
} else { } else {
ratio = (clientX - rect.left) / rect.width ratio = (clientX - rect.left) / rect.width
} }
const raw = min + clamp(ratio, 0, 1) * (max - min) const raw = min + clamp(ratio, 0, 1) * (max - min)
return clamp(snap(raw, step, min), min, max) return clamp(snap(raw, step, min), min, max)
}, },
[isVertical, min, max, step], [isVertical, min, max, step],
) )
const handlePointerDown = ( const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>, handle: 0 | 1) => {
e: ReactPointerEvent<HTMLDivElement>, if (disabled || readOnly) return
handle: 0 | 1, e.currentTarget.setPointerCapture(e.pointerId)
) => { activeHandle.current = handle
if (disabled || readOnly) return dragging.current = true
e.currentTarget.setPointerCapture(e.pointerId) setShowTooltip((t) => {
activeHandle.current = handle const next = [...t]
dragging.current = true next[handle] = true
setShowTooltip((t) => { return next
const next = [...t] })
next[handle] = true }
return next
})
}
const handlePointerMove = useCallback( const handlePointerMove = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => { (e: ReactPointerEvent<HTMLDivElement>) => {
if (!dragging.current || disabled || readOnly) return if (!dragging.current || disabled || readOnly) return
const newVal = valueFromPointer(e.clientX, e.clientY) const newVal = valueFromPointer(e.clientX, e.clientY)
if (range) { if (range) {
const [a, b] = toArray(currentValue)
const next: [number, number] =
activeHandle.current === 0
? [Math.min(newVal, b), b]
: [a, Math.max(newVal, a)]
commit(next)
} else {
commit(newVal)
}
},
[disabled, readOnly, valueFromPointer, range, currentValue, commit],
)
const handlePointerUp = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
if (!dragging.current) return
dragging.current = false
const newVal = valueFromPointer(e.clientX, e.clientY)
const final = range
? (() => {
const [a, b] = toArray(currentValue)
return activeHandle.current === 0
? ([Math.min(newVal, b), b] as [number, number])
: ([a, Math.max(newVal, a)] as [number, number])
})()
: newVal
commit(final, true)
setShowTooltip([false, false])
},
[valueFromPointer, range, currentValue, commit],
)
// Track tıklama (handle dışı)
const handleTrackClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (disabled || readOnly || dragging.current) return
const newVal = valueFromPointer(e.clientX, e.clientY)
if (range) {
const [a, b] = toArray(currentValue)
const distA = Math.abs(newVal - a)
const distB = Math.abs(newVal - b)
const next: [number, number] =
distA <= distB
? [clamp(snap(newVal, step, min), min, b), b]
: [a, clamp(snap(newVal, step, min), a, max)]
commit(next, true)
} else {
commit(newVal, true)
}
},
[disabled, readOnly, valueFromPointer, range, currentValue, step, min, max, commit],
)
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>, handle: 0 | 1) => {
if (disabled || readOnly) return
const [a, b] = toArray(currentValue) const [a, b] = toArray(currentValue)
const cur = handle === 0 ? a : b const next: [number, number] =
let next = cur activeHandle.current === 0 ? [Math.min(newVal, b), b] : [a, Math.max(newVal, a)]
switch (e.key) { commit(next)
case 'ArrowRight': } else {
case 'ArrowUp': commit(newVal)
e.preventDefault(); next = clamp(snap(cur + step, step, min), min, max); break }
case 'ArrowLeft': },
case 'ArrowDown': [disabled, readOnly, valueFromPointer, range, currentValue, commit],
e.preventDefault(); next = clamp(snap(cur - step, step, min), min, max); break )
case 'Home':
e.preventDefault(); next = min; break const handlePointerUp = useCallback(
case 'End': (e: ReactPointerEvent<HTMLDivElement>) => {
e.preventDefault(); next = max; break if (!dragging.current) return
case 'PageUp': dragging.current = false
e.preventDefault(); next = clamp(snap(cur + step * 10, step, min), min, max); break const newVal = valueFromPointer(e.clientX, e.clientY)
case 'PageDown': const final = range
e.preventDefault(); next = clamp(snap(cur - step * 10, step, min), min, max); break ? (() => {
default: return const [a, b] = toArray(currentValue)
} return activeHandle.current === 0
if (range) { ? ([Math.min(newVal, b), b] as [number, number])
const result: [number, number] = : ([a, Math.max(newVal, a)] as [number, number])
handle === 0 ? [Math.min(next, b), b] : [a, Math.max(next, a)] })()
commit(result, true) : newVal
} else { commit(final, true)
commit(next, true) setShowTooltip([false, false])
} },
[valueFromPointer, range, currentValue, commit],
)
// Track tıklama (handle dışı)
const handleTrackClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (disabled || readOnly || dragging.current) return
const newVal = valueFromPointer(e.clientX, e.clientY)
if (range) {
const [a, b] = toArray(currentValue)
const distA = Math.abs(newVal - a)
const distB = Math.abs(newVal - b)
const next: [number, number] =
distA <= distB
? [clamp(snap(newVal, step, min), min, b), b]
: [a, clamp(snap(newVal, step, min), a, max)]
commit(next, true)
} else {
commit(newVal, true)
}
},
[disabled, readOnly, valueFromPointer, range, currentValue, step, min, max, commit],
)
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>, handle: 0 | 1) => {
if (disabled || readOnly) return
const [a, b] = toArray(currentValue)
const cur = handle === 0 ? a : b
let next = cur
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault()
next = clamp(snap(cur + step, step, min), min, max)
break
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault()
next = clamp(snap(cur - step, step, min), min, max)
break
case 'Home':
e.preventDefault()
next = min
break
case 'End':
e.preventDefault()
next = max
break
case 'PageUp':
e.preventDefault()
next = clamp(snap(cur + step * 10, step, min), min, max)
break
case 'PageDown':
e.preventDefault()
next = clamp(snap(cur - step * 10, step, min), min, max)
break
default:
return
} }
if (range) {
// Görsel hesaplamalar const result: [number, number] =
const [v0, v1] = toArray(currentValue) handle === 0 ? [Math.min(next, b), b] : [a, Math.max(next, a)]
const p0 = percentOf(v0) commit(result, true)
const p1 = percentOf(v1) } else {
commit(next, true)
const trackFillStyle = isVertical
? range
? { bottom: `${p0}%`, top: `${100 - p1}%` }
: { bottom: 0, top: `${100 - p0}%` }
: range
? { left: `${p0}%`, right: `${100 - p1}%` }
: { left: 0, width: `${p0}%` }
const handle0Style = isVertical
? { bottom: `calc(${p0}% - ${handleSize / 2}px)` }
: { left: `calc(${p0}% - ${handleSize / 2}px)` }
const handle1Style = isVertical
? { bottom: `calc(${p1}% - ${handleSize / 2}px)` }
: { left: `calc(${p1}% - ${handleSize / 2}px)` }
// Marks hesapla
const resolvedMarks = marks === true
? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({
value: min + i * step,
label: `${min + i * step}`,
}))
: Array.isArray(marks)
? marks
: []
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
const renderHandle = (handle: 0 | 1) => {
const val = handle === 0 ? v0 : v1
const hStyle = handle === 0 ? handle0Style : handle1Style
const show = showTooltip[handle]
return (
<div
key={handle}
className={classNames(
'slider-handle',
ringClass,
disabled && 'slider-handle-disabled',
)}
style={{ ...hStyle, width: handleSize, height: handleSize }}
role="slider"
tabIndex={disabled || readOnly ? -1 : 0}
aria-valuemin={handle === 0 ? min : v0}
aria-valuemax={handle === 1 ? max : v1}
aria-valuenow={val}
aria-disabled={disabled}
aria-readonly={readOnly}
onPointerDown={(e) => handlePointerDown(e, handle)}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onKeyDown={(e) => handleKeyDown(e, handle)}
onFocus={() =>
tooltip && setShowTooltip((t) => { const n = [...t]; n[handle] = true; return n })
}
onBlur={() =>
setShowTooltip((t) => { const n = [...t]; n[handle] = false; return n })
}
>
{(tooltip || show) && show && (
<div className="slider-tooltip">{val}</div>
)}
</div>
)
} }
}
// Görsel hesaplamalar
const [v0, v1] = toArray(currentValue)
const p0 = percentOf(v0)
const p1 = percentOf(v1)
const trackFillStyle = isVertical
? range
? { bottom: `${p0}%`, top: `${100 - p1}%` }
: { bottom: 0, top: `${100 - p0}%` }
: range
? { left: `${p0}%`, right: `${100 - p1}%` }
: { left: 0, width: `${p0}%` }
const handle0Style = isVertical
? { bottom: `calc(${p0}% - ${handleSize / 2}px)` }
: { left: `calc(${p0}% - ${handleSize / 2}px)` }
const handle1Style = isVertical
? { bottom: `calc(${p1}% - ${handleSize / 2}px)` }
: { left: `calc(${p1}% - ${handleSize / 2}px)` }
// Marks hesapla
const resolvedMarks =
marks === true
? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({
value: min + i * step,
label: `${min + i * step}`,
}))
: Array.isArray(marks)
? marks
: []
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
const renderHandle = (handle: 0 | 1) => {
const val = handle === 0 ? v0 : v1
const hStyle = handle === 0 ? handle0Style : handle1Style
const show = showTooltip[handle]
return ( return (
<div <div
ref={ref} key={handle}
className={classNames( className={classNames('slider-handle', ringClass, disabled && 'slider-handle-disabled')}
'slider', style={{ ...hStyle, width: handleSize, height: handleSize }}
isVertical ? 'slider-vertical' : 'slider-horizontal', role="slider"
disabled && 'slider-disabled', tabIndex={disabled || readOnly ? -1 : 0}
className, aria-valuemin={handle === 0 ? min : v0}
)} aria-valuemax={handle === 1 ? max : v1}
style={style} aria-valuenow={val}
{...rest} aria-disabled={disabled}
> aria-readonly={readOnly}
{name && ( onPointerDown={(e) => handlePointerDown(e, handle)}
<input onPointerMove={handlePointerMove}
type="hidden" onPointerUp={handlePointerUp}
name={name ?? field?.name} onKeyDown={(e) => handleKeyDown(e, handle)}
value={Array.isArray(currentValue) ? currentValue.join(',') : currentValue} onFocus={() =>
/> tooltip &&
)} setShowTooltip((t) => {
<div const n = [...t]
ref={trackRef} n[handle] = true
className="slider-track" return n
style={ })
isVertical }
? { width: trackSize } onBlur={() =>
: { height: trackSize } setShowTooltip((t) => {
} const n = [...t]
onClick={handleTrackClick} n[handle] = false
> return n
<div className="slider-fill" style={trackFillStyle} /> })
}
{renderHandle(0)} >
{range && renderHandle(1)} {(tooltip || show) && show && <div className="slider-tooltip">{val}</div>}
</div> </div>
{/* Marks */}
{resolvedMarks.length > 0 && (
<div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}>
{resolvedMarks.map((m) => {
const pct = percentOf(m.value)
const markStyle = isVertical
? { bottom: `${pct}%` }
: { left: `${pct}%` }
const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0
return (
<span key={m.value} className="slider-mark-wrapper" style={markStyle}>
<span
className={classNames(
'slider-mark-dot',
active && 'slider-mark-dot-active',
)}
/>
{m.label && (
<span className="slider-mark-label">{m.label}</span>
)}
</span>
)
})}
</div>
)}
</div>
) )
}
return (
<div
ref={ref}
className={classNames(
'slider',
isVertical ? 'slider-vertical' : 'slider-horizontal',
disabled && 'slider-disabled',
className,
)}
style={style}
{...rest}
>
{name && (
<input
type="hidden"
name={name ?? field?.name}
value={Array.isArray(currentValue) ? currentValue.join(',') : currentValue}
/>
)}
<div
ref={trackRef}
className="slider-track"
style={isVertical ? { width: trackSize } : { height: trackSize }}
onClick={handleTrackClick}
>
<div className="slider-fill" style={trackFillStyle} />
{renderHandle(0)}
{range && renderHandle(1)}
</div>
{/* Marks */}
{resolvedMarks.length > 0 && (
<div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}>
{resolvedMarks.map((m) => {
const pct = percentOf(m.value)
const markStyle = isVertical ? { bottom: `${pct}%` } : { left: `${pct}%` }
const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0
return (
<span key={m.value} className="slider-mark-wrapper" style={markStyle}>
<span
className={classNames('slider-mark-dot', active && 'slider-mark-dot-active')}
/>
{m.label && <span className="slider-mark-label">{m.label}</span>}
</span>
)
})}
</div>
)}
</div>
)
}) })
Slider.displayName = 'Slider' Slider.displayName = 'Slider'

View file

@ -1,220 +1,220 @@
import { NotificationPlacement } from '../@types/placement' import { NotificationPlacement } from '../@types/placement'
type GetPlacementTransitionParams = { type GetPlacementTransitionParams = {
offsetX: string | number offsetX: string | number
offsetY: string | number offsetY: string | number
placement: NotificationPlacement placement: NotificationPlacement
transitionType: 'scale' | 'fade' transitionType: 'scale' | 'fade'
} }
type Motion = { type Motion = {
opacity: number opacity: number
transform?: string transform?: string
} }
type MotionProps = { type MotionProps = {
initial: Motion initial: Motion
animate: Motion animate: Motion
exit: Motion exit: Motion
} }
type MotionDefault = { type MotionDefault = {
top?: string | number top?: string | number
left?: string | number left?: string | number
right?: string | number right?: string | number
bottom?: string | number bottom?: string | number
transform?: string transform?: string
} }
type MotionTransition = { type MotionTransition = {
default: MotionDefault default: MotionDefault
variants: MotionProps variants: MotionProps
} }
export const getPlacementTransition = ({ export const getPlacementTransition = ({
offsetX, offsetX,
offsetY, offsetY,
placement, placement,
transitionType, transitionType,
}: GetPlacementTransitionParams) => { }: GetPlacementTransitionParams) => {
if (transitionType === 'fade') { if (transitionType === 'fade') {
return fadeTransition(offsetX, offsetY)[placement] return fadeTransition(offsetX, offsetY)[placement]
} }
return scaleTransition(offsetX, offsetY)[placement] return scaleTransition(offsetX, offsetY)[placement]
} }
const scaleMotionProps = { const scaleMotionProps = {
initial: { initial: {
opacity: 0, opacity: 0,
transform: 'scale(0.75)', transform: 'scale(0.75)',
}, },
animate: { animate: {
transform: 'scale(1)', transform: 'scale(1)',
opacity: 1, opacity: 1,
}, },
exit: { exit: {
opacity: 0, opacity: 0,
transform: 'scale(0.75)', transform: 'scale(0.75)',
}, },
} }
const fadeMotionProps = { const fadeMotionProps = {
initial: { initial: {
opacity: 0, opacity: 0,
}, },
animate: { animate: {
opacity: 1, opacity: 1,
}, },
exit: { exit: {
opacity: 0, opacity: 0,
}, },
} }
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': {
default: { default: {
top: offsetY, top: offsetY,
right: offsetX, right: offsetX,
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'top-start': { 'top-start': {
default: { default: {
top: offsetY, top: offsetY,
left: offsetX, left: offsetX,
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'top-center': { 'top-center': {
default: { default: {
top: offsetY, top: offsetY,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'middle-center': { 'middle-center': {
default: { default: {
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'bottom-end': { 'bottom-end': {
default: { default: {
bottom: offsetY, bottom: offsetY,
right: offsetX, right: offsetX,
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'bottom-start': { 'bottom-start': {
default: { default: {
bottom: offsetY, bottom: offsetY,
left: offsetX, left: offsetX,
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
'bottom-center': { 'bottom-center': {
default: { default: {
bottom: offsetY, bottom: offsetY,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
}, },
variants: { variants: {
...scaleMotionProps, ...scaleMotionProps,
}, },
}, },
} }
} }
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': {
default: { default: {
top: offsetY, top: offsetY,
right: offsetX, right: offsetX,
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'top-start': { 'top-start': {
default: { default: {
top: offsetY, top: offsetY,
left: offsetX, left: offsetX,
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'top-center': { 'top-center': {
default: { default: {
top: offsetY, top: offsetY,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'middle-center': { 'middle-center': {
default: { default: {
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'bottom-end': { 'bottom-end': {
default: { default: {
bottom: offsetY, bottom: offsetY,
right: offsetX, right: offsetX,
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'bottom-start': { 'bottom-start': {
default: { default: {
bottom: offsetY, bottom: offsetY,
left: offsetX, left: offsetX,
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
'bottom-center': { 'bottom-center': {
default: { default: {
bottom: offsetY, bottom: offsetY,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
}, },
variants: { variants: {
...fadeMotionProps, ...fadeMotionProps,
}, },
}, },
} }
} }

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 (

File diff suppressed because it is too large Load diff

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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.Reconnected')}
type="success"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.Error', { message })}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.Warning', { message })}
type="warning"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.Kicked', { message })}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.Signal.Connected')} type="success" />,
}) {
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.Signal.JoinFailed')} type="danger" />,
}) {
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.LeaveFailed')}
type="warning"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.PublicMessageFailed')}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.PrivateMessageFailed')}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.AnnouncementFailed')} type="danger" />,
}) {
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.MuteFailed')} type="warning" />,
}) {
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.RaiseHandFailed')}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.KickFailed')} type="danger" />,
}) {
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.HandRaiseApproveFailed')}
type="warning"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.LowerHandFailed')}
type="warning"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Signal.DisconnectError')}
type="warning"
/>,
{
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.Rtc.OfferFailed')} type="danger" />,
}) {
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.Rtc.AnswerFailed')} type="danger" />,
}) {
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(
placement: 'bottom-end', <Notification title={this.translate('::App.Videoroom.Rtc.CameraFailed')} type="danger" />,
}) {
placement: 'bottom-end',
},
)
} }
} }
} }
@ -269,9 +291,15 @@ export class WebRTCService {
}) })
} }
} catch { } catch {
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, { toast.push(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Rtc.MicrophoneFailed')}
type="danger"
/>,
{
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(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Rtc.ReconnectFailed')}
type="warning"
/>,
{
placement: 'bottom-end',
},
)
} }
} catch { } catch {
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, { toast.push(
placement: 'bottom-end', <Notification
}) title={this.translate('::App.Videoroom.Rtc.IceRestartFailed')}
type="danger"
/>,
{
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,14 +103,26 @@ 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) {
// eslint-disable-next-line react-hooks/exhaustive-deps loadColumns(dsCode, sp.schemaName, sp.objectName)
return
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dbObjects]) }, [dbObjects])
if (!initialValues) { if (!initialValues) {
@ -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([])
} }
@ -324,7 +340,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
placeholder={translate('::App.Listform.ListformField.TableName')} placeholder={translate('::App.Listform.ListformField.TableName')}
component={Input} component={Input}
/> />
</FormItem> </FormItem>
<FormItem <FormItem
label={translate('::App.Listform.ListformField.KeyFieldName')} label={translate('::App.Listform.ListformField.KeyFieldName')}
invalid={errors.keyFieldName && touched.keyFieldName} invalid={errors.keyFieldName && touched.keyFieldName}
@ -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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::ListForms.ListFormField.FieldCopied')}
</Notification>,
{
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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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' },
@ -278,7 +282,7 @@ function JsonRowOpDialogCommand({
component={Input} component={Input}
/> />
</FormItem> </FormItem>
<Tabs defaultValue="urlOptions" variant="pill"> <Tabs defaultValue="urlOptions" variant="pill">
<TabList> <TabList>
<TabNav value="urlOptions">Url Options</TabNav> <TabNav value="urlOptions">Url Options</TabNav>
@ -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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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(
placement: 'bottom-end', <Notification type="success">
}) {translate('::App.Platform.RecordDeleted')}
</Notification>,
{
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

@ -135,358 +135,126 @@ function JsonRowOpDialogSeries({
> >
{(data.operation === 'create' || data.operation === 'update') && ( {(data.operation === 'create' || data.operation === 'update') && (
<Formik <Formik
initialValues={ initialValues={
data.chartSeriesValues ?? { data.chartSeriesValues ?? {
index: -1, index: -1,
argumentField: '', argumentField: '',
axis: '', axis: '',
barOverlapGroup: '', barOverlapGroup: '',
barPadding: 0, barPadding: 0,
barWidth: 0, barWidth: 0,
color: '', color: '',
cornerRadius: 0, cornerRadius: 0,
dashStyle: 'solid', dashStyle: 'solid',
ignoreEmptyPoints: false, ignoreEmptyPoints: false,
name: '', name: '',
pane: '', pane: '',
rangeValue1Field: '', rangeValue1Field: '',
rangeValue2Field: '', rangeValue2Field: '',
selectionMode: 'none', selectionMode: 'none',
showInLegend: true, showInLegend: true,
type: 'line', type: 'line',
valueField: '', valueField: '',
visible: true,
width: 2,
label: {
visible: true, visible: true,
width: 2, backgroundColor: '#f05b41',
label: { customizeText: '',
visible: true, format: 'decimal',
backgroundColor: '#f05b41', font: {
customizeText: '', color: '#FFFFFF',
format: 'decimal', family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
font: { size: 12,
color: '#FFFFFF', weight: 400,
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
size: 12,
weight: 400,
},
}, },
} },
} }
validationSchema={schema} }
onSubmit={async (values, { setSubmitting }) => { validationSchema={schema}
setSubmitting(true) onSubmit={async (values, { setSubmitting }) => {
try { setSubmitting(true)
const input: ListFormJsonRowDto = { try {
index: data.index, const input: ListFormJsonRowDto = {
fieldName: data.tabName, index: data.index,
itemChartSeries: values, fieldName: data.tabName,
} itemChartSeries: values,
if (data.index === -1) {
await postListFormJsonRow(data.id, input)
} else {
await putListFormJsonRow(data.id, input)
}
toast.push(
<Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
</Notification>,
{ placement: 'bottom-end' },
)
handleClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setSubmitting(false)
} }
}} if (data.index === -1) {
> await postListFormJsonRow(data.id, input)
{({ touched, errors, values, isSubmitting }) => ( } else {
<Form className="flex flex-col h-full"> await putListFormJsonRow(data.id, input)
<Dialog.Body className="flex flex-col gap-2 overflow-hidden"> }
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5> toast.push(
<div className="flex-1 overflow-y-auto p-2"> <Notification type="success">
<div className="grid grid-cols-2 gap-6"> {translate(
{/* Sol Kolon */} data.index === -1
<div className="flex flex-col gap-1"> ? '::App.Platform.RecordAdded'
<FormItem : '::App.Platform.RecordUpdated',
label="Visible" )}
invalid={errors.visible && touched.visible} </Notification>,
errorMessage={errors.visible} { placement: 'bottom-end' },
> )
<Field name="visible" component={Checkbox} /> handleClose()
</FormItem> } catch (error: any) {
toast.push(
<FormItem <Notification type="danger">
label="Show In Legend" {translate('::App.Platform.Error')}
invalid={errors.showInLegend && touched.showInLegend} <code>{error}</code>
errorMessage={errors.showInLegend} </Notification>,
> { placement: 'bottom-end' },
<Field name="showInLegend" component={Checkbox} /> )
</FormItem> } finally {
setSubmitting(false)
<FormItem }
label="Ignore Empty Points" }}
invalid={errors.ignoreEmptyPoints && touched.ignoreEmptyPoints} >
errorMessage={errors.ignoreEmptyPoints} {({ touched, errors, values, isSubmitting }) => (
> <Form className="flex flex-col h-full">
<Field name="ignoreEmptyPoints" component={Checkbox} /> <Dialog.Body className="flex flex-col gap-2 overflow-hidden">
</FormItem> <h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
<div className="flex-1 overflow-y-auto p-2">
<FormItem <div className="grid grid-cols-2 gap-6">
label="Type" {/* Sol Kolon */}
invalid={errors.type && touched.type} <div className="flex flex-col gap-1">
errorMessage={errors.type} <FormItem
> label="Visible"
<Field type="text" name="type"> invalid={errors.visible && touched.visible}
{({ field, form }: FieldProps<SelectBoxOption>) => ( errorMessage={errors.visible}
<Select
field={field}
form={form}
options={chartSeriesTypeOptions}
isClearable={true}
value={chartSeriesTypeOptions.filter(
(option) => option.value === values.type,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Name"
invalid={errors.name && touched.name}
errorMessage={errors.name}
>
<Field type="text" name="name" component={Input} />
</FormItem>
<FormItem
label="Argument Field"
invalid={errors.argumentField && touched.argumentField}
errorMessage={errors.argumentField}
>
<Field type="text" name="argumentField">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === values.argumentField,
)}
menuPlacement="auto"
maxMenuHeight={150}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Value Field"
invalid={errors.valueField && touched.valueField}
errorMessage={errors.valueField}
>
<Field type="text" name="valueField">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find((option) => option.value === values.valueField)}
menuPlacement="auto"
maxMenuHeight={150}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormFieldEdit.SummaryType')}
invalid={errors.summaryType && touched.summaryType}
errorMessage={errors.summaryType}
>
<Field
type="text"
autoComplete="off"
name="summaryType"
placeholder={translate('::ListForms.ListFormFieldEdit.SummaryType')}
> >
{({ field, form }: FieldProps<SelectBoxOption>) => ( <Field name="visible" component={Checkbox} />
<Select
field={field}
form={form}
isClearable={true}
options={columnSummaryTypeListOptions}
value={columnSummaryTypeListOptions.filter(
(option: any) => option.value === values.summaryType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Axis"
invalid={errors.axis && touched.axis}
errorMessage={errors.axis}
>
<Field type="text" name="axis">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={valueAxisList()}
value={valueAxisList()?.find((option) => option.value === values.axis)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Pane"
invalid={errors.pane && touched.pane}
errorMessage={errors.pane}
>
<Field type="text" name="pane">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={valuePaneList()}
value={valuePaneList()?.find((option) => option.value === values.pane)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Dash Style"
invalid={errors.dashStyle && touched.dashStyle}
errorMessage={errors.dashStyle}
>
<Field type="text" name="dashStyle">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={chartSeriesDashStyleOptions}
isClearable={true}
value={chartSeriesDashStyleOptions.filter(
(option) => option.value === values.dashStyle,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Color"
invalid={errors.color && touched.color}
errorMessage={errors.color}
>
<Field name="color">
{({ field, form }: FieldProps) => (
<Input
{...field}
type="color"
className='!h-10 !p-0 !m-0 !border-0'
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Selection Mode"
invalid={errors.selectionMode && touched.selectionMode}
errorMessage={errors.selectionMode}
>
<Field type="text" name="selectionMode">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={chartSeriesSelectionModeOptions}
isClearable={true}
value={chartSeriesSelectionModeOptions.filter(
(option) => option.value === values.selectionMode,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Width"
invalid={errors.width && touched.width}
errorMessage={errors.width}
>
<Field type="number" name="width" component={Input} />
</FormItem>
<FormItem
label="Corner Radius"
invalid={errors.cornerRadius && touched.cornerRadius}
errorMessage={errors.cornerRadius}
>
<Field type="number" name="cornerRadius" component={Input} />
</FormItem>
</div>
{/* Sağ Kolon - Label */}
<div className="flex flex-col gap-1">
<Card className="my-2" header="Label">
<FormItem label="Label Visible">
<Field name="label.visible" component={Checkbox} />
</FormItem> </FormItem>
<FormItem <FormItem
label="Background Color" label="Show In Legend"
invalid={errors.label?.backgroundColor && touched.label?.backgroundColor} invalid={errors.showInLegend && touched.showInLegend}
errorMessage={errors.label?.backgroundColor} errorMessage={errors.showInLegend}
> >
<Field name="label.backgroundColor"> <Field name="showInLegend" component={Checkbox} />
{({ field, form }: FieldProps) => (
<Input
{...field}
type="color"
className='!h-10 !p-0 !m-0 !border-0'
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/>
)}
</Field>
</FormItem> </FormItem>
<FormItem <FormItem
label="Format" label="Ignore Empty Points"
invalid={errors.label?.format && touched.label?.format} invalid={errors.ignoreEmptyPoints && touched.ignoreEmptyPoints}
errorMessage={errors.label?.format} errorMessage={errors.ignoreEmptyPoints}
> >
<Field type="text" name="label.format"> <Field name="ignoreEmptyPoints" component={Checkbox} />
</FormItem>
<FormItem
label="Type"
invalid={errors.type && touched.type}
errorMessage={errors.type}
>
<Field type="text" name="type">
{({ field, form }: FieldProps<SelectBoxOption>) => ( {({ field, form }: FieldProps<SelectBoxOption>) => (
<Select <Select
field={field} field={field}
form={form} form={form}
options={tooltipFormatListOptions} options={chartSeriesTypeOptions}
isClearable={true} isClearable={true}
value={tooltipFormatListOptions.filter( value={chartSeriesTypeOptions.filter(
(option) => option.value === values.label.format, (option) => option.value === values.type,
)} )}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)}
/> />
@ -495,97 +263,344 @@ function JsonRowOpDialogSeries({
</FormItem> </FormItem>
<FormItem <FormItem
label="Customize Text" label="Name"
invalid={errors.label?.customizeText && touched.label?.customizeText} invalid={errors.name && touched.name}
errorMessage={errors.label?.customizeText} errorMessage={errors.name}
> >
<Field type="text" name="label.customizeText" component={Input} /> <Field type="text" name="name" component={Input} />
</FormItem> </FormItem>
<Card className="my-2" header="Font"> <FormItem
<FormItem label="Color"> label="Argument Field"
<Field name="label.font.color"> invalid={errors.argumentField && touched.argumentField}
errorMessage={errors.argumentField}
>
<Field type="text" name="argumentField">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === values.argumentField,
)}
menuPlacement="auto"
maxMenuHeight={150}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Value Field"
invalid={errors.valueField && touched.valueField}
errorMessage={errors.valueField}
>
<Field type="text" name="valueField">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === values.valueField,
)}
menuPlacement="auto"
maxMenuHeight={150}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormFieldEdit.SummaryType')}
invalid={errors.summaryType && touched.summaryType}
errorMessage={errors.summaryType}
>
<Field
type="text"
autoComplete="off"
name="summaryType"
placeholder={translate('::ListForms.ListFormFieldEdit.SummaryType')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={columnSummaryTypeListOptions}
value={columnSummaryTypeListOptions.filter(
(option: any) => option.value === values.summaryType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Axis"
invalid={errors.axis && touched.axis}
errorMessage={errors.axis}
>
<Field type="text" name="axis">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={valueAxisList()}
value={valueAxisList()?.find(
(option) => option.value === values.axis,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Pane"
invalid={errors.pane && touched.pane}
errorMessage={errors.pane}
>
<Field type="text" name="pane">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={valuePaneList()}
value={valuePaneList()?.find(
(option) => option.value === values.pane,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Dash Style"
invalid={errors.dashStyle && touched.dashStyle}
errorMessage={errors.dashStyle}
>
<Field type="text" name="dashStyle">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={chartSeriesDashStyleOptions}
isClearable={true}
value={chartSeriesDashStyleOptions.filter(
(option) => option.value === values.dashStyle,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Color"
invalid={errors.color && touched.color}
errorMessage={errors.color}
>
<Field name="color">
{({ field, form }: FieldProps) => (
<Input
{...field}
type="color"
className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Selection Mode"
invalid={errors.selectionMode && touched.selectionMode}
errorMessage={errors.selectionMode}
>
<Field type="text" name="selectionMode">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={chartSeriesSelectionModeOptions}
isClearable={true}
value={chartSeriesSelectionModeOptions.filter(
(option) => option.value === values.selectionMode,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Width"
invalid={errors.width && touched.width}
errorMessage={errors.width}
>
<Field type="number" name="width" component={Input} />
</FormItem>
<FormItem
label="Corner Radius"
invalid={errors.cornerRadius && touched.cornerRadius}
errorMessage={errors.cornerRadius}
>
<Field type="number" name="cornerRadius" component={Input} />
</FormItem>
</div>
{/* Sağ Kolon - Label */}
<div className="flex flex-col gap-1">
<Card className="my-2" header="Label">
<FormItem label="Label Visible">
<Field name="label.visible" component={Checkbox} />
</FormItem>
<FormItem
label="Background Color"
invalid={errors.label?.backgroundColor && touched.label?.backgroundColor}
errorMessage={errors.label?.backgroundColor}
>
<Field name="label.backgroundColor">
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<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)}
/> />
)} )}
</Field> </Field>
</FormItem> </FormItem>
<FormItem label="Family"> <FormItem
<Field name="label.font.family" component={Input} /> label="Format"
invalid={errors.label?.format && touched.label?.format}
errorMessage={errors.label?.format}
>
<Field type="text" name="label.format">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={tooltipFormatListOptions}
isClearable={true}
value={tooltipFormatListOptions.filter(
(option) => option.value === values.label.format,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem> </FormItem>
<FormItem label="Size"> <FormItem
<Field type="number" name="label.font.size" component={Input} /> label="Customize Text"
invalid={errors.label?.customizeText && touched.label?.customizeText}
errorMessage={errors.label?.customizeText}
>
<Field type="text" name="label.customizeText" component={Input} />
</FormItem> </FormItem>
<FormItem label="Weight"> <Card className="my-2" header="Font">
<Field type="number" name="label.font.weight" component={Input} /> <FormItem label="Color">
</FormItem> <Field name="label.font.color">
{({ field, form }: FieldProps) => (
<Input
{...field}
type="color"
className="!h-10 !p-0 !m-0 !border-0"
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
/>
)}
</Field>
</FormItem>
<FormItem label="Family">
<Field name="label.font.family" component={Input} />
</FormItem>
<FormItem label="Size">
<Field type="number" name="label.font.size" component={Input} />
</FormItem>
<FormItem label="Weight">
<Field type="number" name="label.font.weight" component={Input} />
</FormItem>
</Card>
</Card> </Card>
</Card>
</div>
</div> </div>
</div> </div>
</Dialog.Body> </div>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> </Dialog.Body>
<Button size="sm" variant="plain" onClick={handleClose}> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
{translate('::Cancel')} <Button size="sm" variant="plain" onClick={handleClose}>
</Button> {translate('::Cancel')}
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> </Button>
{isSubmitting ? translate('::Saving') : translate('::Save')} <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? translate('::Saving') : translate('::Save')}
</Dialog.Footer> </Button>
</Form> </Dialog.Footer>
)} </Form>
</Formik> )}
</Formik>
)} )}
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<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() )
} catch (error: any) { handleClose()
toast.push( } catch (error: any) {
<Notification type="danger"> toast.push(
Hata <Notification type="danger">
<code>{error}</code> {translate('::App.Platform.Error')}
</Notification>, <code>{error}</code>
{ placement: 'bottom-end' }, </Notification>,
) { placement: 'bottom-end' },
} finally { )
setSubmitting(false) } finally {
} setSubmitting(false)
}} }
> }}
{({ isSubmitting }) => ( >
<Form className="flex flex-col h-full"> {({ isSubmitting }) => (
<Dialog.Body className="flex flex-col gap-2"> <Form className="flex flex-col h-full">
<h5>{translate('::Delete')}</h5> <Dialog.Body className="flex flex-col gap-2">
<p>{translate('::DeleteConfirmation')}</p> <h5>{translate('::Delete')}</h5>
</Dialog.Body> <p>{translate('::DeleteConfirmation')}</p>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> </Dialog.Body>
<Button size="sm" variant="plain" onClick={handleClose}> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
{translate('::Cancel')} <Button size="sm" variant="plain" onClick={handleClose}>
</Button> {translate('::Cancel')}
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> </Button>
{isSubmitting ? 'Deleting' : 'Delete'} <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? 'Deleting' : 'Delete'}
</Dialog.Footer> </Button>
</Form> </Dialog.Footer>
)} </Form>
</Formik> )}
</Formik>
)} )}
</Dialog> </Dialog>
) )

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

@ -84,398 +84,403 @@ function JsonRowOpDialogWidget({
> >
{(data.operation === 'create' || data.operation === 'update') && ( {(data.operation === 'create' || data.operation === 'update') && (
<Formik <Formik
initialValues={ initialValues={
data.widgetValues ?? { data.widgetValues ?? {
colGap: 3, colGap: 3,
colSpan: 3, colSpan: 3,
sqlQuery: sqlQuery:
"SELECT 'Total Records' as title, COUNT(*) as value, 'blue' as color, 'FaChartBar' as icon, 'Active records' as subTitle FROM YourTable WHERE IsActive = 1", "SELECT 'Total Records' as title, COUNT(*) as value, 'blue' as color, 'FaChartBar' as icon, 'Active records' as subTitle FROM YourTable WHERE IsActive = 1",
className: 'mb-3', className: 'mb-3',
valueClassName: 'bg-5 text-sm', valueClassName: 'bg-5 text-sm',
title: 'title', title: 'title',
value: 'value', value: 'value',
color: 'color', color: 'color',
icon: 'icon', icon: 'icon',
subTitle: 'subTitle', subTitle: 'subTitle',
onClick: '', onClick: '',
isActive: true, isActive: true,
}
} }
validationSchema={schema} }
onSubmit={async (values, { setSubmitting }) => { validationSchema={schema}
setSubmitting(true) onSubmit={async (values, { setSubmitting }) => {
try { setSubmitting(true)
const input: ListFormJsonRowDto = { try {
index: data.index, const input: ListFormJsonRowDto = {
fieldName: data.tabName, index: data.index,
itemWidget: values, fieldName: data.tabName,
} itemWidget: values,
if (data.index === -1) {
await postListFormJsonRow(data.id, input)
} else {
await putListFormJsonRow(data.id, input)
}
toast.push(
<Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
</Notification>,
{ placement: 'bottom-end' },
)
handleClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setSubmitting(false)
} }
}} if (data.index === -1) {
> await postListFormJsonRow(data.id, input)
{({ touched, errors, values, isSubmitting, setFieldValue }) => ( } else {
<Form className="flex flex-col h-full"> await putListFormJsonRow(data.id, input)
<Dialog.Body className="flex flex-col gap-2 overflow-hidden"> }
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5> toast.push(
<div className="flex-1 overflow-y-auto p-1"> <Notification type="success">
<div className="grid grid-cols-5 gap-4"> {translate(
<FormItem data.index === -1
label="Column Gap" ? '::App.Platform.RecordAdded'
invalid={errors.colGap && touched.colGap} : '::App.Platform.RecordUpdated',
errorMessage={errors.colGap} )}
> </Notification>,
<Field { placement: 'bottom-end' },
type="number" )
autoComplete="off" handleClose()
name="colGap" } catch (error: any) {
placeholder="Column Gap" toast.push(
component={Input} <Notification type="danger">
/> {translate('::App.Platform.Error')}
</FormItem> <code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setSubmitting(false)
}
}}
>
{({ touched, errors, values, isSubmitting, setFieldValue }) => (
<Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
<div className="flex-1 overflow-y-auto p-1">
<div className="grid grid-cols-5 gap-4">
<FormItem
label="Column Gap"
invalid={errors.colGap && touched.colGap}
errorMessage={errors.colGap}
>
<Field
type="number"
autoComplete="off"
name="colGap"
placeholder="Column Gap"
component={Input}
/>
</FormItem>
<FormItem <FormItem
label="Column Span" label="Column Span"
invalid={errors.colSpan && touched.colSpan} invalid={errors.colSpan && touched.colSpan}
errorMessage={errors.colSpan} errorMessage={errors.colSpan}
> >
<Field type="text" autoComplete="off" name="colSpan" placeholder="colSpan"> <Field type="text" autoComplete="off" name="colSpan" placeholder="colSpan">
{({ field, form }: FieldProps<SelectBoxOption>) => ( {({ field, form }: FieldProps<SelectBoxOption>) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable={true} isClearable={true}
options={colSpanOptions} options={colSpanOptions}
value={colSpanOptions?.filter( value={colSpanOptions?.filter(
(option: any) => option.value === values.colSpan, (option: any) => option.value === values.colSpan,
)} )}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Widget Container CSS Classes</div>
<div>Examples: mb-3, mt-2, p-4, rounded-lg, shadow-md</div>
</div>
}
>
<FormItem
label="Class Name"
invalid={errors.className && touched.className}
errorMessage={errors.className}
>
<Field
type="text"
autoComplete="off"
name="className"
placeholder="Tailwind CSS classes: mb-3, mt-2, p-4..."
component={Input}
/> />
</FormItem> )}
</Tooltip> </Field>
</FormItem>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Value Display CSS Classes</div>
<div>Examples: text-3xl, text-2xl, font-bold, text-sm, text-center</div>
</div>
}
>
<FormItem
label="Value Class Name"
invalid={errors.valueClassName && touched.valueClassName}
errorMessage={errors.valueClassName}
>
<Field
type="text"
autoComplete="off"
name="valueClassName"
placeholder="Tailwind CSS classes: text-3xl, font-bold..."
component={Input}
/>
</FormItem>
</Tooltip>
<FormItem
label="IsActive"
invalid={errors.isActive && touched.isActive}
errorMessage={errors.isActive}
>
<Field
autoComplete="off"
name="isActive"
placeholder="IsActive"
component={Checkbox}
/>
</FormItem>
</div>
<Tooltip <Tooltip
title={ title={
<div className="text-xs max-h-96 overflow-y-auto"> <div className="text-xs">
<div className="font-semibold mb-2">SQL Query Examples:</div> <div className="font-semibold mb-1">Widget Container CSS Classes</div>
<div className="space-y-3"> <div>Examples: mb-3, mt-2, p-4, rounded-lg, shadow-md</div>
<div>
<code className="text-xs bg-gray-800 p-1 rounded block mt-1">
{
"SELECT 'Aktif' as title, COUNT(Id) as value, 'blue' as color, 'FaChartBar' as icon, 'Aktif kayıtlar' as subTitle FROM YourTable WHERE IsActive = 1"
}
</code>
</div>
</div>
</div> </div>
} }
> >
Sql Query <FormItem
label="Class Name"
invalid={errors.className && touched.className}
errorMessage={errors.className}
>
<Field
type="text"
autoComplete="off"
name="className"
placeholder="Tailwind CSS classes: mb-3, mt-2, p-4..."
component={Input}
/>
</FormItem>
</Tooltip>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Value Display CSS Classes</div>
<div>Examples: text-3xl, text-2xl, font-bold, text-sm, text-center</div>
</div>
}
>
<FormItem
label="Value Class Name"
invalid={errors.valueClassName && touched.valueClassName}
errorMessage={errors.valueClassName}
>
<Field
type="text"
autoComplete="off"
name="valueClassName"
placeholder="Tailwind CSS classes: text-3xl, font-bold..."
component={Input}
/>
</FormItem>
</Tooltip> </Tooltip>
<FormItem <FormItem
invalid={errors.sqlQuery && touched.sqlQuery} label="IsActive"
errorMessage={errors.sqlQuery} invalid={errors.isActive && touched.isActive}
errorMessage={errors.isActive}
> >
<div <Field
className="border rounded-lg overflow-hidden" autoComplete="off"
style={{ height: '35vh' }} name="isActive"
> placeholder="IsActive"
<SqlEditor component={Checkbox}
value={values.sqlQuery || ''} />
height="35vh"
onChange={(value) => setFieldValue('sqlQuery', value || '')}
/>
</div>
</FormItem> </FormItem>
</div>
<div className="grid grid-cols-5 gap-2"> <Tooltip
<FormItem title={
label="Title Field" <div className="text-xs max-h-96 overflow-y-auto">
invalid={errors.title && touched.title} <div className="font-semibold mb-2">SQL Query Examples:</div>
errorMessage={errors.title} <div className="space-y-3">
> <div>
<Field <code className="text-xs bg-gray-800 p-1 rounded block mt-1">
type="text" {
autoComplete="off" "SELECT 'Aktif' as title, COUNT(Id) as value, 'blue' as color, 'FaChartBar' as icon, 'Aktif kayıtlar' as subTitle FROM YourTable WHERE IsActive = 1"
name="title" }
placeholder="Column name from SQL query e.g., 'title'" </code>
component={Input}
/>
</FormItem>
<FormItem
label="Value Field"
invalid={errors.value && touched.value}
errorMessage={errors.value}
>
<Field
type="text"
autoComplete="off"
name="value"
placeholder="Column name from SQL query e.g., 'value'"
component={Input}
/>
</FormItem>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Available Colors:</div>
<div className="grid grid-cols-2 gap-1">
<div> blue</div>
<div> green</div>
<div> purple</div>
<div> gray</div>
<div> red</div>
<div> yellow</div>
<div> pink</div>
<div> indigo</div>
<div> teal</div>
<div> orange</div>
</div>
</div> </div>
} </div>
> </div>
<FormItem }
label="Color Field" >
invalid={errors.color && touched.color} Sql Query
errorMessage={errors.color} </Tooltip>
>
<Field
type="text"
autoComplete="off"
name="color"
placeholder="blue, green, purple, gray, red..."
component={Input}
/>
</FormItem>
</Tooltip>
<Tooltip <FormItem
title={ invalid={errors.sqlQuery && touched.sqlQuery}
<div className="text-xs max-h-64 overflow-y-auto"> errorMessage={errors.sqlQuery}
<div className="font-semibold mb-2">Popular Icon Examples:</div> >
<div className="space-y-1"> <div className="border rounded-lg overflow-hidden" style={{ height: '35vh' }}>
<div> <SqlEditor
📊 <strong>Charts:</strong> FaChartBar, FaChartLine, FaChartPie, value={values.sqlQuery || ''}
FaChartArea height="35vh"
</div> onChange={(value) => setFieldValue('sqlQuery', value || '')}
<div> />
💰 <strong>Finance:</strong> FaDollarSign, FaMoneyBill, FaWallet,
FaCreditCard
</div>
<div>
👥 <strong>Users:</strong> FaUser, FaUsers, FaUserCircle, FaUserTie
</div>
<div>
📦 <strong>Business:</strong> FaShoppingCart, FaBoxes, FaWarehouse,
FaTruck
</div>
<div>
📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown,
FaArrowTrendUp
</div>
<div>
<strong>Settings:</strong> FaCog, FaTools, FaWrench
</div>
<div>
🔔 <strong>Alerts:</strong> FaBell, FaExclamation, FaInfoCircle
</div>
<div>
📁 <strong>Files:</strong> FaFile, FaFolder, FaFileAlt, FaDownload
</div>
<div>
🏠 <strong>Other:</strong> FaHome, FaBuilding, FaGlobe, FaHeart
</div>
</div>
<div className="mt-2 pt-2 border-t">
All icons from <strong>react-icons/fa</strong> (Font Awesome)
</div>
</div>
}
>
<FormItem
label="Icon Field"
invalid={errors.icon && touched.icon}
errorMessage={errors.icon}
>
<Field
type="text"
autoComplete="off"
name="icon"
placeholder="FaChartBar, FaUsers, FaShoppingCart..."
component={Input}
/>
</FormItem>
</Tooltip>
<FormItem
label="Sub Title Field"
invalid={errors.subTitle && touched.subTitle}
errorMessage={errors.subTitle}
>
<Field
type="text"
autoComplete="off"
name="subTitle"
placeholder="Column name from SQL query e.g., 'subTitle'"
component={Input}
/>
</FormItem>
</div> </div>
</FormItem>
<div className="grid grid-cols-5 gap-2">
<FormItem <FormItem
label="On Click" label="Title Field"
invalid={errors.onClick && touched.onClick} invalid={errors.title && touched.title}
errorMessage={errors.onClick} errorMessage={errors.title}
> >
<Field <Field
type="text" type="text"
autoComplete="off" autoComplete="off"
name="onClick" name="title"
placeholder="JavaScript function: () => alert('Clicked')" placeholder="Column name from SQL query e.g., 'title'"
component={Input}
/>
</FormItem>
<FormItem
label="Value Field"
invalid={errors.value && touched.value}
errorMessage={errors.value}
>
<Field
type="text"
autoComplete="off"
name="value"
placeholder="Column name from SQL query e.g., 'value'"
component={Input}
/>
</FormItem>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Available Colors:</div>
<div className="grid grid-cols-2 gap-1">
<div> blue</div>
<div> green</div>
<div> purple</div>
<div> gray</div>
<div> red</div>
<div> yellow</div>
<div> pink</div>
<div> indigo</div>
<div> teal</div>
<div> orange</div>
</div>
</div>
}
>
<FormItem
label="Color Field"
invalid={errors.color && touched.color}
errorMessage={errors.color}
>
<Field
type="text"
autoComplete="off"
name="color"
placeholder="blue, green, purple, gray, red..."
component={Input}
/>
</FormItem>
</Tooltip>
<Tooltip
title={
<div className="text-xs max-h-64 overflow-y-auto">
<div className="font-semibold mb-2">Popular Icon Examples:</div>
<div className="space-y-1">
<div>
📊 <strong>Charts:</strong> FaChartBar, FaChartLine, FaChartPie,
FaChartArea
</div>
<div>
💰 <strong>Finance:</strong> FaDollarSign, FaMoneyBill, FaWallet,
FaCreditCard
</div>
<div>
👥 <strong>Users:</strong> FaUser, FaUsers, FaUserCircle, FaUserTie
</div>
<div>
📦 <strong>Business:</strong> FaShoppingCart, FaBoxes, FaWarehouse,
FaTruck
</div>
<div>
📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown, FaArrowTrendUp
</div>
<div>
<strong>Settings:</strong> FaCog, FaTools, FaWrench
</div>
<div>
🔔 <strong>Alerts:</strong> FaBell, FaExclamation, FaInfoCircle
</div>
<div>
📁 <strong>Files:</strong> FaFile, FaFolder, FaFileAlt, FaDownload
</div>
<div>
🏠 <strong>Other:</strong> FaHome, FaBuilding, FaGlobe, FaHeart
</div>
</div>
<div className="mt-2 pt-2 border-t">
All icons from <strong>react-icons/fa</strong> (Font Awesome)
</div>
</div>
}
>
<FormItem
label="Icon Field"
invalid={errors.icon && touched.icon}
errorMessage={errors.icon}
>
<Field
type="text"
autoComplete="off"
name="icon"
placeholder="FaChartBar, FaUsers, FaShoppingCart..."
component={Input}
/>
</FormItem>
</Tooltip>
<FormItem
label="Sub Title Field"
invalid={errors.subTitle && touched.subTitle}
errorMessage={errors.subTitle}
>
<Field
type="text"
autoComplete="off"
name="subTitle"
placeholder="Column name from SQL query e.g., 'subTitle'"
component={Input} component={Input}
/> />
</FormItem> </FormItem>
</div> </div>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> <FormItem
<Button size="sm" variant="plain" onClick={handleClose}> label="On Click"
{translate('::Cancel')} invalid={errors.onClick && touched.onClick}
</Button> errorMessage={errors.onClick}
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> >
{isSubmitting ? translate('::Saving') : translate('::Save')} <Field
</Button> type="text"
</Dialog.Footer> autoComplete="off"
</Form> name="onClick"
)} placeholder="JavaScript function: () => alert('Clicked')"
</Formik> component={Input}
/>
</FormItem>
</div>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
<Button size="sm" variant="plain" onClick={handleClose}>
{translate('::Cancel')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>
</Dialog.Footer>
</Form>
)}
</Formik>
)} )}
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<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() )
} catch (error: any) { handleClose()
toast.push( } catch (error: any) {
<Notification type="danger"> toast.push(
Hata <Notification type="danger">
<code>{error}</code> {translate('::App.Platform.Error')}
</Notification>, <code>{error}</code>
{ placement: 'bottom-end' }, </Notification>,
) { placement: 'bottom-end' },
} finally { )
setSubmitting(false) } finally {
} setSubmitting(false)
// getListFormJsonRow() }
}} // getListFormJsonRow()
> }}
{({ isSubmitting }) => ( >
<Form className="flex flex-col h-full"> {({ isSubmitting }) => (
<Dialog.Body className="flex flex-col gap-2"> <Form className="flex flex-col h-full">
<h5>{translate('::Delete')}</h5> <Dialog.Body className="flex flex-col gap-2">
<p>{translate('::DeleteConfirmation')}</p> <h5>{translate('::Delete')}</h5>
</Dialog.Body> <p>{translate('::DeleteConfirmation')}</p>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> </Dialog.Body>
<Button size="sm" variant="plain" onClick={handleClose}> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
{translate('::Cancel')} <Button size="sm" variant="plain" onClick={handleClose}>
</Button> {translate('::Cancel')}
<Button size="sm" variant="solid" loading={isSubmitting} type="submit"> </Button>
{isSubmitting ? 'Deleting' : 'Delete'} <Button size="sm" variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? 'Deleting' : 'Delete'}
</Dialog.Footer> </Button>
</Form> </Dialog.Footer>
)} </Form>
</Formik> )}
</Formik>
)} )}
</Dialog> </Dialog>
) )

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,23 +169,30 @@ const StringListControl = ({
spec: OptionSpec spec: OptionSpec
value: unknown value: unknown
onChange: (v: unknown) => void onChange: (v: unknown) => void
}) => ( }) => {
<input const { translate } = useLocalization()
className={controlClass}
value={Array.isArray(value) ? value.join(', ') : leafToText(value)} return (
placeholder={spec.placeholder} <input
onChange={(event) => { className={controlClass}
const items = event.target.value value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
.split(',') placeholder={spec.placeholder ? translate('::' + spec.placeholder) : undefined}
.map((item) => item.trim()) onChange={(event) => {
.filter(Boolean) const items = event.target.value
onChange(items.length ? items : undefined) .split(',')
}} .map((item) => item.trim())
/> .filter(Boolean)
) 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