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

View file

@ -1,6 +1,7 @@
import { useMemo, useState } from 'react'
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
import { Button } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface StyleModalProps {
isOpen: boolean
@ -12,30 +13,348 @@ interface StyleModalProps {
}
const STYLE_GROUPS = {
Display: ['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'contents', 'hidden'],
Layout: ['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'],
Display: [
'block',
'inline-block',
'inline',
'flex',
'inline-flex',
'grid',
'inline-grid',
'contents',
'hidden',
],
Layout: [
'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
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 [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 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()
return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query))
}, [category, search])
@ -56,41 +375,112 @@ const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose
}
return (
<div 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="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>
<h2 className="font-semibold text-slate-900 dark:text-white">StyleModal · {propertyName}</h2>
<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>
<h2 className="font-semibold text-slate-900 dark:text-white">
StyleModal · {propertyName}
</h2>
<p className="mt-1 text-xs text-slate-500">
{translate(
mode === 'css' ? '::App.StyleModal.CssModeHint' : '::App.StyleModal.ClassModeHint',
)}
</p>
</div>
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
</div>
{mode === 'class' && <div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
{mode === 'class' && (
<div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
<label className="relative">
<FaSearch className="absolute left-3 top-3 text-slate-400" />
<input className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Hazır sınıflarda ara…" value={search} onChange={(event) => setSearch(event.target.value)} />
<input
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
placeholder={translate('::App.StyleModal.SearchClasses')}
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</label>
<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)}>
<option value="Tümü">Tüm kategoriler</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="Örn. hover:scale-105 w-[320px] [&>span]:font-bold" value={customClass} onChange={(event) => setCustomClass(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addCustomClass() }} />
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>Ekle</Button>
</div>}
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">{propertyName} değerini elle düzenle</label>
<textarea className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`} placeholder={mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'} value={value} onChange={(event) => onChange(event.target.value)} />
</div>
{mode === 'class' && <div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
{classes.map((className) => (
<button key={className} className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`} type="button" onClick={() => toggleClass(className)}>{className}</button>
<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)}
>
<option value="__all__">{translate('::App.StyleModal.AllCategories')}</option>
{Object.keys(STYLE_GROUPS).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</div>}
</select>
</div>
)}
{mode === 'class' && (
<div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700">
<input
className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white"
placeholder={translate('::App.StyleModal.CustomClassPlaceholder')}
value={customClass}
onChange={(event) => setCustomClass(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') addCustomClass()
}}
/>
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>
{translate('::ListForms.Wizard.Add')}
</Button>
</div>
)}
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">
{translate('::App.StyleModal.EditValueManually', { property: propertyName })}
</label>
<textarea
className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`}
placeholder={
mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'
}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</div>
{mode === 'class' && (
<div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
{classes.map((className) => (
<button
key={className}
className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`}
type="button"
onClick={() => toggleClass(className)}
>
{className}
</button>
))}
</div>
)}
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
<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>
<div className="flex gap-2"><Button size="sm" variant="default" onClick={() => onChange('')}>Temizle</Button><Button size="sm" variant="solid" onClick={onClose}>Tamam</Button></div>
<span className="text-xs text-slate-500">
{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>

File diff suppressed because it is too large Load diff

View file

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

View file

@ -3,6 +3,7 @@ import DynamicRenderer from './DynamicRenderer'
import { useComponents } from '@/contexts/ComponentContext'
import { parseComponentDependencies } from '@/contexts/componentRuntime'
import { Loading } from '../shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
export interface ComponentPreviewProps {
componentName?: string
@ -10,6 +11,7 @@ export interface ComponentPreviewProps {
}
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
const { translate } = useLocalization()
const { components, loading } = useComponents()
// 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])
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)) {

View file

@ -1,34 +1,34 @@
import React from "react";
import type { ErrorInfo } from "react";
import React from 'react'
import type { ErrorInfo } from 'react'
interface Props {
children: React.ReactNode;
children: React.ReactNode
}
interface State {
hasError: boolean;
error: Error | null;
hasError: boolean
error: Error | null
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
return { hasError: true, error }
}
componentDidUpdate(prevProps: Props) {
// Eğer component değişmişse, hata state'ini sıfırla
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) {
console.error("Render hatası:", error, info);
console.error('Render hatası:', error, info)
}
render() {
@ -36,27 +36,27 @@ class ErrorBoundary extends React.Component<Props, State> {
return (
<div
style={{
color: "red",
background: "#fff0f0",
border: "1px solid #f44336",
padding: "1rem",
borderRadius: "8px",
fontFamily: "monospace",
whiteSpace: "pre-wrap",
color: 'red',
background: '#fff0f0',
border: '1px solid #f44336',
padding: '1rem',
borderRadius: '8px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
}}
>
Render Hatası:
Render error:
<br />
<details open style={{ marginTop: "0.5rem" }}>
<summary>Detayları Göster</summary>
<details open style={{ marginTop: '0.5rem' }}>
<summary>Show details</summary>
{this.state.error?.message || String(this.state.error)}
</details>
</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 [generating, setGenerating] = useState(false)
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set())
const [sessionExecutes, setSessionExecutes] = useState<
Record<string, ListFormImportLogDto[]>
>({})
const [sessionExecutes, setSessionExecutes] = useState<Record<string, ListFormImportLogDto[]>>({})
const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set())
useEffect(() => {
@ -414,7 +412,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{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">
{column.fieldName}
</td>
@ -740,8 +741,14 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
size="xs"
>
<span>
{execute.errorRows} hata detayı
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
{translate('::App.Listforms.ImportManager.ErrorDetails', {
count: execute.errorRows,
})}{' '}
{translate(
expandedErrors.has(execute.id)
? '::App.Platform.Hide'
: '::App.Platform.Show',
)}
</span>
</Button>
@ -752,10 +759,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<thead className="bg-orange-100 dark:bg-gray-800 sticky top-0">
<tr>
<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 className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
Hata Mesajı
{translate('::App.Platform.ErrorMessage')}
</th>
</tr>
</thead>
@ -777,7 +784,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</table>
) : (
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
Hata detayı mevcut değil.
{translate(
'::App.Listforms.ImportManager.NoErrorDetail',
)}
</p>
)}
</div>

View file

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

View file

@ -38,10 +38,7 @@ const layouts = {
const AuthLayout = lazy(() => import('./AuthLayout'))
const PublicLayout = lazy(() => import('./PublicLayout'))
class LayoutChunkErrorBoundary extends Component<
{ children: ReactNode },
{ failed: boolean }
> {
class LayoutChunkErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false }
static getDerivedStateFromError() {
@ -57,16 +54,21 @@ class LayoutChunkErrorBoundary extends Component<
return (
<div className="flex min-h-screen items-center justify-center p-6">
<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">
Bağlantınızı kontrol edip uygulamayı yeniden yükleyin.
Check your connection and reload the application.
</p>
<button
type="button"
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white"
onClick={() => window.location.reload()}
>
Yeniden yükle
Reload
</button>
</div>
</div>

View file

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

View file

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

View file

@ -53,9 +53,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
size="xs"
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
title={
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
}
title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
>
{translate('::Public.products.billingcycle.monthly')}
</Button>
@ -65,9 +63,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
size="xs"
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
title={
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
}
title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
>
{translate('::Public.products.billingcycle.yearly')}
</Button>
@ -90,11 +86,13 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
icon={<FaMinus className="w-4 h-4" />}
variant="default"
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">
<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">
{globalBillingCycle === 'monthly'
? translate('::Public.products.billingcycle.month')
@ -108,7 +106,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
icon={<FaPlus className="w-4 h-4" />}
variant="default"
size="xs"
title={hasBasketItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
title={hasBasketItems ? translate('::Public.basket.periodLocked') : undefined}
/>
</div>
</div>

View file

@ -1,5 +1,12 @@
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 { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -42,8 +49,8 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
const getItemPrice = (item: BasketItem) =>
item.billingCycle === 'monthly'
? item.product.monthlyPrice ?? 0
: item.product.yearlyPrice ?? 0
? (item.product.monthlyPrice ?? 0)
: (item.product.yearlyPrice ?? 0)
const vatRateLabel = (() => {
if (!order) return ''
@ -117,7 +124,9 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
</h3>
<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="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>
<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">{item.quantity}</div>
<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 className="col-span-2 text-right">
%{((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>
</div>
<div className="flex justify-between">
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span>
<span>
{translate('::Public.products.kdv')}
{vatRateLabel}
</span>
<span>{formatPrice(order.vatTotal)}</span>
</div>
<div className="flex justify-between">

View file

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

View file

@ -114,9 +114,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
</span>
</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 */}
<div className="mb-4 space-y-3">
@ -157,15 +161,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
<div className="mb-4">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{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 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>
{globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPriceWithVat())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getPeriodText()}</span>
{translate('::App.Listform.ListformField.Total')}{' '}
{formatPrice(getTotalPriceWithVat())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
{getPeriodText()}
</span>
</div>
)}
{globalPeriod <= 1 && (
@ -188,7 +198,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
size="md"
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>
)
})()}

View file

@ -91,8 +91,12 @@ const getNationalPhoneDigits = (value: string, country?: CountryDto) => {
return digits
}
const getPhoneValidationError = (value: string, country?: CountryDto) => {
if (!country) return 'Lütfen önce ülke seçin.'
const getPhoneValidationError = (
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 minLength = getPhoneMinLength(country)
@ -100,8 +104,8 @@ const getPhoneValidationError = (value: string, country?: CountryDto) => {
if (length >= minLength && length <= maxLength) return ''
return minLength === maxLength
? `Telefon numarası ${minLength} haneli olmalıdır.`
: `Telefon numarası ${minLength}-${maxLength} hane arasında olmalıdır.`
? translate('::App.Tenant.PhoneExactLength', { length: minLength })
: translate('::App.Tenant.PhoneRangeLength', { min: minLength, max: maxLength })
}
const formatPhoneNumber = (value: string, country?: CountryDto) => {
@ -309,7 +313,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
if (isExisting) {
if (!formData.name?.trim()) {
setExistingTenantError('Lütfen önce organization code girin.')
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return
}
@ -318,12 +322,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
!formData.organizationName
) {
setExistingTenantError('Devam etmek için önce organization code ile kurum bilgisini bulun.')
setExistingTenantError(translate('::App.Tenant.FindOrgBeforeContinue'))
return
}
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
}
@ -338,23 +342,27 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
}
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
setFormError('Lütfen zorunlu alanları eksiksiz doldurun.')
setFormError(translate('::App.Tenant.FillRequiredFields'))
hasValidationError = true
}
const nextPhoneError = getPhoneValidationError(formData.phoneNumber || '', selectedCountry)
const nextPhoneError = getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
)
setPhoneError(nextPhoneError)
if (nextPhoneError) {
hasValidationError = true
}
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
setEmailError('Lütfen geçerli bir e-posta adresi girin.')
setEmailError(translate('::App.Platform.InvalidEmail'))
hasValidationError = true
}
if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) {
setWebsiteError('Lütfen geçerli bir web sitesi adresi girin.')
setWebsiteError(translate('::App.Tenant.InvalidWebsite'))
hasValidationError = true
}
@ -499,7 +507,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const getTenantInfo = async () => {
const tenantName = formData.name?.trim()
if (!tenantName) {
setExistingTenantError('Lütfen önce organization code girin.')
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return
}
@ -535,7 +543,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
if (tenant.data.name) setFoundTenantName(tenant.data.name)
} catch (error) {
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)
} finally {
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"
>
{isLoadingTenant
? 'Aranıyor...'
? translate('::App.Tenant.Searching')
: translate('::Public.products.tenantForm.searchOrg')}
</Button>
</div>
@ -647,7 +655,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
!canSubmitExistingTenant &&
!isLoadingTenant && (
<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>
)}
@ -807,7 +815,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
countryOptions.find((option) => option.value === formData.country) || null
}
onChange={handleCountryChange}
placeholder="Ülke seçin"
placeholder={translate('::App.Tenant.SelectCountry')}
/>
</div>
</div>
@ -827,7 +835,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
options={cityOptions}
value={cityOptions.find((option) => option.value === formData.city) || null}
onChange={handleCityChange}
placeholder={formData.country ? 'Şehir seçin' : 'Önce ülke seçin'}
placeholder={translate(
formData.country
? '::App.Tenant.SelectCity'
: '::App.Tenant.SelectCountryFirstShort',
)}
/>
</div>
</div>
@ -847,7 +859,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
null
}
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>
@ -870,7 +886,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
) || null
}
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>
@ -922,7 +942,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
required
inputMode="numeric"
placeholder={phoneMask.replace(/0/g, '_')}
aria-label={`Telefon numarası, format: ${phoneMask}`}
aria-label={translate('::App.Tenant.PhoneAria', { mask: phoneMask })}
value={formData.phoneNumber || ''}
onChange={(e) => {
handleInputChange(
@ -933,7 +953,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
}}
onBlur={() =>
setPhoneError(
getPhoneValidationError(formData.phoneNumber || '', selectedCountry),
getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
),
)
}
aria-invalid={Boolean(phoneError)}

View file

@ -1,4 +1,5 @@
import { Button, Dialog } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Editor, { type Monaco } from '@monaco-editor/react'
import type * as monacoApi from 'monaco-editor'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -69,9 +70,12 @@ function ScriptBuilderDialog({
dialect,
onClose,
onApply,
saveLabel = 'Kaydet',
cancelLabel = 'Vazgeç',
saveLabel,
cancelLabel,
}: ScriptBuilderDialogProps) {
const { translate } = useLocalization()
const saveText = saveLabel ?? translate('::Save')
const cancelText = cancelLabel ?? translate('::Cancel')
const [rules, setRules] = useState<ScriptRule[]>([])
const [code, setCode] = useState('')
// 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
* 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) {
updateRule(rule.id, { condition: { ...rule.condition, ...patch } })
return
@ -191,7 +199,10 @@ function ScriptBuilderDialog({
const removeCondition = (rule: ScriptRule, index: number) => {
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) =>
@ -314,7 +325,7 @@ function ScriptBuilderDialog({
updateRule(rule.id, { fields: [...selectedFields, next] })
}}
>
<option value="">+ ekle</option>
<option value="">{translate('::App.ScriptBuilder.AddOption')}</option>
{options
.filter((fieldName) => !selectedFields.includes(fieldName))
.map((fieldName) => (
@ -341,7 +352,7 @@ function ScriptBuilderDialog({
param.optionSource,
currentValue,
(next) => updateParam(rule.id, param.key, next),
param.placeholder || 'Seç',
param.placeholder || translate('::App.ScriptBuilder.Choose'),
)}
{param.type === 'select' && (
<select
@ -384,7 +395,9 @@ function ScriptBuilderDialog({
<div key={index} className="grid grid-cols-12 items-end gap-2">
{index > 0 ? (
<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
className={controlClass}
value={rule.join ?? 'and'}
@ -392,19 +405,25 @@ function ScriptBuilderDialog({
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] })
}
>
<option value="and">VE</option>
<option value="or">VEYA</option>
<option value="and">{translate('::App.ScriptBuilder.And')}</option>
<option value="or">{translate('::App.ScriptBuilder.Or')}</option>
</select>
</label>
) : (
<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>
<div className="flex h-9 items-center text-xs font-semibold text-gray-400">EĞER</div>
<span className="mb-1 block text-xs text-gray-500">
{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>
)}
<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
className={controlClass}
value={condition.operator}
@ -412,7 +431,7 @@ function ScriptBuilderDialog({
>
{dialect.operators.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
{item.label.startsWith('App.') ? translate('::' + item.label) : item.label}
</option>
))}
</select>
@ -420,7 +439,9 @@ function ScriptBuilderDialog({
{operator?.needsSource && dialect.conditionKinds.length > 1 && (
<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
className={controlClass}
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''}
@ -439,19 +460,23 @@ function ScriptBuilderDialog({
{operator?.needsSource && (
<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(
kind?.optionSource,
condition.source,
(next) => updateCondition(rule, index, { source: next }),
kind?.placeholder || 'Seç',
kind?.placeholder || translate('::App.ScriptBuilder.Choose'),
)}
</label>
)}
{operator?.needsValue && (
<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
className={controlClass}
value={condition.value}
@ -468,7 +493,7 @@ function ScriptBuilderDialog({
size="xs"
type="button"
variant="plain"
title="Koşulu kaldır"
title={translate('::App.ScriptBuilder.RemoveCondition')}
icon={<FaTimes />}
onClick={() => removeCondition(rule, index)}
/>
@ -511,7 +536,8 @@ function ScriptBuilderDialog({
</div>
{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">
{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>
)}
{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"
title={
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>
)}
<div className="flex shrink-0 items-center">
@ -539,7 +565,7 @@ function ScriptBuilderDialog({
size="xs"
type="button"
variant="plain"
title="Yukarı taşı"
title={translate('::App.Platform.MoveUp')}
icon={<FaArrowUp />}
onClick={() => moveRule(index, -1)}
/>
@ -548,7 +574,7 @@ function ScriptBuilderDialog({
size="xs"
type="button"
variant="plain"
title="Aşağı taşı"
title={translate('::App.Platform.MoveDown')}
icon={<FaArrowDown />}
onClick={() => moveRule(index, 1)}
/>
@ -557,7 +583,7 @@ function ScriptBuilderDialog({
size="xs"
type="button"
variant="plain"
title="Kuralı sil"
title={translate('::App.ScriptBuilder.DeleteRule')}
icon={<FaTrash />}
onClick={() => touchRules((current) => current.filter((item) => item.id !== rule.id))}
/>
@ -595,10 +621,10 @@ function ScriptBuilderDialog({
size="xs"
type="button"
icon={<FaPlus />}
title="Aynı kurala ikinci bir koşul ekler; VE/VEYA ile birleşir."
title={translate('::App.ScriptBuilder.AddConditionHint')}
onClick={() => addCondition(rule)}
>
Koşul ekle
{translate('::App.ScriptBuilder.AddCondition')}
</Button>
</div>
</div>
@ -622,7 +648,7 @@ function ScriptBuilderDialog({
<FaSearch className="text-gray-400" size={11} />
<input
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}
onChange={(event) => setHelpSearch(event.target.value)}
/>
@ -631,7 +657,7 @@ function ScriptBuilderDialog({
size="xs"
type="button"
variant="plain"
title="Paneli kapat"
title={translate('::App.Platform.ClosePanel')}
icon={<FaTimes />}
onClick={() => setHelpOpen(false)}
/>
@ -656,7 +682,9 @@ function ScriptBuilderDialog({
</button>
))}
{!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 className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
@ -678,7 +706,9 @@ function ScriptBuilderDialog({
</button>
))}
{!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>
@ -728,15 +758,15 @@ function ScriptBuilderDialog({
<Button
size="sm"
type="button"
title="Kısayol ve snippet panelini aç"
title={translate('::App.ScriptBuilder.OpenHelpPanel')}
icon={<FaBook />}
onClick={() => setHelpOpen(true)}
>
Yardım
{translate('::App.ScriptBuilder.Help')}
</Button>
)}
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyScript}>
{copied ? 'Kopyalandı' : 'Kopyala'}
{translate(copied ? '::App.Platform.Copied' : '::App.SqlQueryManager.Copy')}
</Button>
<div className="flex overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
<button
@ -757,10 +787,10 @@ function ScriptBuilderDialog({
? 'bg-indigo-500 text-white'
: '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}
>
<FaMagic /> Sihirbaz
<FaMagic /> {translate('::App.ScriptBuilder.Wizard')}
</button>
</div>
</div>
@ -769,12 +799,9 @@ function ScriptBuilderDialog({
{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">
<FaExclamationTriangle className="shrink-0" />
<span className="flex-1">
Koddaki script kurallardan farklı. Burada bir kuralı değiştirdiğin anda kod
kurallardan yeniden üretilir.
</span>
<span className="flex-1">{translate('::App.ScriptBuilder.CodeDivergedWarning')}</span>
<Button size="xs" type="button" onClick={() => setView('code')}>
Kodu gör
{translate('::App.ScriptBuilder.ShowCode')}
</Button>
</div>
)}
@ -789,11 +816,11 @@ function ScriptBuilderDialog({
disabled={!generatedScript}
size="xs"
type="button"
title="Sihirbazdaki kurallardan script'i yeniden üretir."
title={translate('::App.ScriptBuilder.RegenerateHint')}
icon={<FaMagic />}
onClick={() => setCode(generatedScript)}
>
Kurallardan üret
{translate('::App.ScriptBuilder.Regenerate')}
</Button>
)}
</div>
@ -824,7 +851,9 @@ function ScriptBuilderDialog({
{paletteOpen ? <FaChevronDown size={11} /> : <FaChevronRight size={11} />}
</span>
<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>
{paletteOpen && (
<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}
size="sm"
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)}
>
{recipe.label}
@ -866,8 +895,7 @@ function ScriptBuilderDialog({
{!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">
Henüz kural yok. Yukarıdan bir kural ekle; her kural tek satırlık script üretir ve
kod sekmesine anında yansır.
{translate('::App.ScriptBuilder.NoRulesYet')}
</div>
)}
</section>
@ -876,10 +904,14 @@ function ScriptBuilderDialog({
<div className="mb-3 flex items-center justify-between gap-2">
<span className="flex items-center gap-2 text-sm font-semibold">
<FaCode className="text-gray-400" />
Script Önizleme
{translate('::App.ScriptBuilder.ScriptPreview')}
</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">
{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>
</div>
<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%"
language="javascript"
theme="vs-dark"
value={generatedScript || '// Kural ekledikçe script burada oluşur'}
value={
generatedScript || `// ${translate('::App.ScriptBuilder.EmptyScriptHint')}`
}
options={{ ...editorOptions, readOnly: true }}
/>
</div>
<p className="mt-2 text-[11px] text-gray-400">
İlk satırdaki <code>{'// @builder'}</code> yorumu kuralları saklar; dialog yeniden
ıldığında sihirbaz kuralları buradan geri yükler.
{translate('::App.ScriptBuilder.BuilderCommentHintPrefix')}{' '}
<code>{'// @builder'}</code>{' '}
{translate('::App.ScriptBuilder.BuilderCommentHintSuffix')}
</p>
</section>
</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">
<Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}>
{cancelLabel}
{cancelText}
</Button>
<Button
size="sm"
@ -914,7 +949,7 @@ function ScriptBuilderDialog({
onClose()
}}
>
{saveLabel}
{saveText}
</Button>
</Dialog.Footer>
</Dialog>

View file

@ -161,13 +161,38 @@ export const scriptRuleConditions = (rule: ScriptRule): ScriptRuleCondition[] =>
export const DEFAULT_SCRIPT_OPERATORS: ScriptOperator[] = [
{ 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: 'contains', label: 'İçeriyorsa', needsValue: true, needsSource: true },
{ value: 'empty', label: 'Boşsa', needsSource: true },
{
value: 'equals',
label: 'App.ScriptBuilder.Operator.Equals',
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: 'greaterThan', label: 'Büyükse', needsValue: true, needsSource: true },
{ value: 'lessThan', label: 'Küçükse', needsValue: true, needsSource: true },
{ value: 'isTrue', label: 'İşaretliyse', needsSource: true },
{ value: 'isFalse', label: 'İşaretli değilse', needsSource: true },
{
value: 'greaterThan',
label: 'App.ScriptBuilder.Operator.GreaterThan',
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
}
const label = translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'
const label = translate('::ListForms.ListForm.DbMigrate')
return (
<>
@ -39,7 +39,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
icon={<FcAcceptDatabase />}
className={className}
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}
</Button>
@ -49,7 +49,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
type="info"
title={label}
cancelText={translate('::Cancel')}
confirmText={translate('::App.Platform.Execute') || 'Çalıştır'}
confirmText={translate('::App.Platform.Execute')}
onCancel={() => setIsConfirmOpen(false)}
onClose={() => setIsConfirmOpen(false)}
onConfirm={() => {
@ -58,8 +58,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
}}
>
<p className="text-gray-600 dark:text-gray-400">
{translate('::App.DbMigrate.ConfirmMessage') ||
'Are you sure you want to start the database migration process?'}
{translate('::App.DbMigrate.ConfirmMessage')}
</p>
</ConfirmDialog>
</>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,26 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react'
export function usePWA(): boolean {
const [isPWA, setIsPWA] = useState(false);
const [isPWA, setIsPWA] = useState(false)
useEffect(() => {
const checkPWA = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone;
setIsPWA(isStandalone);
};
const 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
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA)
return () => {
// Event listener'ı temizle
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
};
}, []);
return isPWA;
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA)
}
}, [])
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 = [
{ value: "Start", label: "Başlat" },
{ value: "Compare", label: "Karşılaştırma" },
{ value: "Approval", label: "Onaylanacak kişi" },
{ value: "Inform", label: "Bilgilendirme" },
{ value: "End", label: "Akışı bitir" },
];
{ value: 'Start', label: 'ListForms.Workflow.Kind.Start' },
{ value: 'Compare', label: 'ListForms.Workflow.Kind.Compare' },
{ value: 'Approval', label: 'ListForms.Workflow.Kind.Approval' },
{ value: 'Inform', label: 'ListForms.Workflow.Kind.Inform' },
{ value: 'End', label: 'ListForms.Workflow.Kind.End' },
]
export const operatorOptions = [">", ">=", "<", "<=", "=", "!="].map(
(value) => ({
export const operatorOptions = ['>', '>=', '<', '<=', '=', '!='].map((value) => ({
value,
label: value,
}),
);
}))
export const kindIcon: Record<string, any> = {
Start: FiPlay as any,
@ -21,13 +19,13 @@ export const kindIcon: Record<string, any> = {
Approval: FiCheck as any,
Inform: FiBell as any,
End: FiSlash as any,
};
}
export const nodeSize = {
width: 176,
height: 128,
};
}
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
}
addLink(links, criteria, source, source.nextOnStart, 'Sonraki', 'next', {
addLink(links, criteria, source, source.nextOnStart, OUTCOME_KEYS.nextOnStart, 'next', {
index: 0,
count: 1,
field: 'nextOnStart',
})
addLink(links, criteria, source, source.nextOnTrue, 'Doğru', 'true', {
addLink(links, criteria, source, source.nextOnTrue, OUTCOME_KEYS.nextOnTrue, 'true', {
index: 0,
count: 2,
field: 'nextOnTrue',
})
addLink(links, criteria, source, source.nextOnFalse, 'Yanlış', 'false', {
addLink(links, criteria, source, source.nextOnFalse, OUTCOME_KEYS.nextOnFalse, 'false', {
index: 1,
count: 2,
field: 'nextOnFalse',
})
addLink(links, criteria, source, source.nextOnApprove, 'Onay', 'approve', {
addLink(links, criteria, source, source.nextOnApprove, OUTCOME_KEYS.nextOnApprove, 'approve', {
index: 0,
count: 2,
field: 'nextOnApprove',
})
addLink(links, criteria, source, source.nextOnReject, 'Red', 'reject', {
addLink(links, criteria, source, source.nextOnReject, OUTCOME_KEYS.nextOnReject, 'reject', {
index: 1,
count: 2,
field: 'nextOnReject',
@ -242,43 +242,53 @@ function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
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[] {
if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes?.length
? item.compareOutcomes
: [
{ label: 'Doğru', targetId: item.nextOnTrue },
{ label: 'Yanlış', targetId: item.nextOnFalse },
{ label: OUTCOME_KEYS.nextOnTrue, targetId: item.nextOnTrue },
{ label: OUTCOME_KEYS.nextOnFalse, targetId: item.nextOnFalse },
]
return outcomes.slice(0, 4).map((outcome, index) => ({
field: `compareOutcomes:${index}`,
label: outcome.label || `Durum ${index + 1}`,
label: outcome.label || `#${index + 1}`,
targetId: outcome.targetId,
}))
}
if (item.kind === 'Approval') {
return [
{ field: 'nextOnApprove', label: 'Onay', targetId: item.nextOnApprove },
{ field: 'nextOnReject', label: 'Red', targetId: item.nextOnReject },
{ field: 'nextOnApprove', label: OUTCOME_KEYS.nextOnApprove, targetId: item.nextOnApprove },
{ field: 'nextOnReject', label: OUTCOME_KEYS.nextOnReject, targetId: item.nextOnReject },
]
}
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) {
if (field?.startsWith('compareOutcomes:')) return 'Karşılaştırma durumu'
if (field?.startsWith('compareOutcomes:')) return OUTCOME_KEYS.compare
const labels: Record<string, string> = {
nextOnStart: 'Sonraki',
nextOnTrue: 'Doğru',
nextOnFalse: 'Yanlış',
nextOnApprove: 'Onay',
nextOnReject: 'Red',
nextOnStart: OUTCOME_KEYS.nextOnStart,
nextOnTrue: OUTCOME_KEYS.nextOnTrue,
nextOnFalse: OUTCOME_KEYS.nextOnFalse,
nextOnApprove: OUTCOME_KEYS.nextOnApprove,
nextOnReject: OUTCOME_KEYS.nextOnReject,
}
return field ? labels[field] : undefined
@ -406,7 +416,9 @@ function formatCriteriaId(number: number) {
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
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) {
@ -432,8 +444,7 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
}
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson =
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
const sharedPerson = item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
const compareOutcomes = (item.compareOutcomes || [])
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
@ -465,12 +476,12 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
export function defaultTitle(kind: string) {
return (
{
Start: 'İş Akışı Başlat',
Compare: 'Karşılaştırma',
Approval: 'Onay',
Inform: 'Bilgilendirme',
End: 'İş Akışı Bitir',
}[kind] ?? 'İş Akışı Adımı'
Start: 'ListForms.Workflow.Kind.Start',
Compare: 'ListForms.Workflow.Kind.Compare',
Approval: 'ListForms.Workflow.Kind.Approval',
Inform: 'ListForms.Workflow.Kind.Inform',
End: 'ListForms.Workflow.Kind.End',
}[kind] ?? 'ListForms.Workflow.Kind.Step'
)
}
@ -570,7 +581,7 @@ export function compareOutcomeRuleText(
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
)
.join(' ve ')
: 'Kural yok'
: 'ListForms.Workflow.NoRule'
}
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
* 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.
*
* 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 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 { FaArrowLeft } from 'react-icons/fa'
import { Navigate, useLocation } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DialogProvider from './shared/DialogContext'
import DialogShowComponent from './shared/DialogContext/DialogShowComponent'
import UiDialog from './shared/UiDialog'
@ -20,13 +21,16 @@ interface ViewsProps {
// react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor;
// 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 } =
error instanceof Error ? error : { name: 'Hata!', message: String(error) }
error instanceof Error
? error
: { name: translate('::App.Platform.Error'), message: String(error) }
return (
<Alert showIcon className="mb-4" type="danger">
<h5>{name ?? 'Hata!'}</h5>
<h5>{name ?? translate('::App.Platform.Error')}</h5>
<div>{message}</div>
<Button
icon={<FaArrowLeft />}
@ -49,7 +53,7 @@ const Views = (props: ViewsProps) => {
}, [location, setWarning])
return (
<ErrorBoundary fallbackRender={fallbackRender}>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loading loading={true} />}>
{!!warning?.length && (
<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 classNames from 'classnames'
import dayjs from 'dayjs'
import { useLocalization } from '@/utils/hooks/useLocalization'
type EventProps = {
data: NotificationDto
@ -19,6 +20,8 @@ const HighlightedText = ({ children, className }: CommonProps) => {
}
const Event = ({ data, compact }: EventProps) => {
const { translate } = useLocalization()
return (
<>
{compact ? (
@ -28,7 +31,11 @@ const Event = ({ data, compact }: EventProps) => {
<span className="text-xs">{dayjs(data.creationTime).format('hh:mm A')}</span>
</div>
<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
prefix
@ -42,7 +49,8 @@ const Event = ({ data, compact }: EventProps) => {
) : (
<div className="my-1 flex items-center">
<div>
<HighlightedText>{data.notificationType}</HighlightedText> bildirimi gönderdi.
<HighlightedText>{data.notificationType}</HighlightedText>{' '}
{translate('::Abp.Identity.ActivityLogs.SentNotification')}
</div>
<Tag

View file

@ -35,7 +35,9 @@ const Log = ({
</div>
<Timeline className="dark:bg-gray-800">
{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) => (
<Timeline.Item
@ -56,11 +58,13 @@ const Log = ({
))}
<div className="text-center">
{loadable ? (
<Button size="sm" loading={isLoading} variant='solid' onClick={onLoadMore}>
<Button size="sm" loading={isLoading} variant="solid" onClick={onLoadMore}>
{translate('::Abp.Identity.ActivityLogs.LoadMore')}
</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>

View file

@ -44,8 +44,15 @@ const LogFilter = ({
const { translate } = useLocalization()
const content = (
<div 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>
<div
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
vertical
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>
// (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)
return (
@ -250,16 +258,16 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
navigator.clipboard.writeText(fileUrl).then(
() => {
toast.push(
<Notification type="success" title="Başarılı">
Dosya URL&apos;si panoya kopyalandı
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::FileManager.UrlCopied')}
</Notification>,
{ placement: 'bottom-end' },
)
},
() => {
toast.push(
<Notification type="danger" title="Hata">
URL kopyalanamadı
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::FileManager.UrlCopyFailed')}
</Notification>,
{ placement: 'bottom-end' },
)
@ -438,7 +446,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</span>
) : item.type === 'folder' && typeof item.childCount === 'number' ? (
<span className="text-sm text-gray-500 dark:text-gray-400">
{item.childCount} öğe
{translate('::FileManager.ItemCount', { count: item.childCount })}
</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}>
<div ref={ref}>
<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
autoFocus
value={folderName}
@ -58,11 +62,11 @@ export const CreateFolderModal = forwardRef<HTMLDivElement, CreateFolderModalPro
</form>
<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')}
</Button>
<Button
size='sm'
size="sm"
variant="solid"
loading={loading}
disabled={!folderName.trim()}
@ -156,11 +160,11 @@ export const RenameItemModal = forwardRef<HTMLDivElement, RenameItemModalProps>(
</form>
<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')}
</Button>
<Button
size='sm'
size="sm"
variant="solid"
loading={loading}
disabled={!newName.trim()}
@ -239,15 +243,10 @@ export const DeleteConfirmModal = forwardRef<HTMLDivElement, DeleteConfirmModalP
</div>
<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')}
</Button>
<Button
size='sm'
variant="solid"
loading={loading}
onClick={handleDelete}
>
<Button size="sm" variant="solid" loading={loading} onClick={handleDelete}>
{translate('::Delete')}
</Button>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,7 +50,7 @@ function FormTabTree(props: FormEditProps) {
} catch (error: any) {
toast.push(
<Notification type="danger" duration={2000}>
Alanlar getirilemedi
{translate('::App.Platform.FieldsFetchFailed')}
{error.toString()}
</Notification>,
{
@ -184,7 +184,14 @@ function FormTabTree(props: FormEditProps) {
component={Checkbox}
/>
</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')}
</Button>
</FormContainer>

View file

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

View file

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

View file

@ -78,7 +78,7 @@ function TablePickerModal({
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{step === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
? translate('::ListForms.Wizard.Step3.SelectTable')
: (pickerTable?.tableName ?? '')}
</span>
</div>
@ -99,7 +99,7 @@ function TablePickerModal({
<input
autoFocus
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"
onChange={(e) => setTableSearch(e.target.value)}
/>
@ -107,7 +107,7 @@ function TablePickerModal({
<div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') || 'Tablo bulunamadı'}
{translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
</div>
) : (
dbObjects.tables
@ -160,14 +160,14 @@ function TablePickerModal({
<>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu
{translate('::ListForms.ListFormFieldEdit.KeyColumn')}
</label>
<select
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"
onChange={(e) => setKeyCol(e.target.value)}
>
<option value="">-- Seçiniz --</option>
<option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
@ -177,14 +177,14 @@ function TablePickerModal({
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu
{translate('::ListForms.ListFormFieldEdit.NameColumn')}
</label>
<select
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"
onChange={(e) => setNameCol(e.target.value)}
>
<option value="">-- Seçiniz --</option>
<option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={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"
onClick={openTablePicker}
>
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
{translate('::ListForms.Wizard.Step3.GenerateFromTable')}
</Button>
}
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
@ -430,7 +430,14 @@ function FormFieldTabLookup({
</FormItem>
</Card>
</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')}
</Button>
</Form>

View file

@ -154,7 +154,7 @@ function FormFieldTabValidationRules({
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -216,7 +216,7 @@ function FormFieldTabValidationRules({
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -235,10 +235,7 @@ function FormFieldTabValidationRules({
invalid={errors.type && touched.type}
errorMessage={errors.type}
>
<SelectField
name="type"
options={columnValidationTypeListOptions}
/>
<SelectField name="type" options={columnValidationTypeListOptions} />
</FormItem>
<FormItem
@ -328,7 +325,14 @@ function FormFieldTabValidationRules({
<Field name="trim" placeholder="Trim" component={Checkbox} />
</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')}
</Button>
</FormContainer>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -121,7 +121,11 @@ function JsonRowOpDialogDatabase({
}
toast.push(
<Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
{translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>,
{ placement: 'bottom-end' },
)
@ -129,7 +133,7 @@ function JsonRowOpDialogDatabase({
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -239,7 +243,12 @@ function JsonRowOpDialogDatabase({
)}
</div>
<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')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -255,21 +264,26 @@ function JsonRowOpDialogDatabase({
{data.operation === 'delete' && (
<>
<h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p>
<p>{translate('::DeleteConfirmation')}</p>
<Formik
initialValues={data}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
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',
})
},
)
handleClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -284,7 +298,12 @@ function JsonRowOpDialogDatabase({
<Form>
<FormContainer size="sm">
<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')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">

View file

@ -56,7 +56,11 @@ function JsonRowOpDialogEditForm({
onSubmit={async (values, { setSubmitting }) => {
try {
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()
} finally {
setSubmitting(false)
@ -65,24 +69,46 @@ function JsonRowOpDialogEditForm({
>
{({ isSubmitting }) => (
<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">
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Delete')}</Button>
<Button type="button" variant="plain" onClick={handleClose}>
{translate('::Cancel')}
</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>
{translate('::Delete')}
</Button>
</Dialog.Footer>
</Form>
)}
</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}
onSubmit={async (values, { setSubmitting }) => {
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)
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()
} finally {
setSubmitting(false)
@ -92,19 +118,44 @@ function JsonRowOpDialogEditForm({
{({ errors, touched, isSubmitting }) => (
<Form>
<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} />
</FormItem>
<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 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.Footer className="flex justify-end gap-2">
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Save')}</Button>
<Button type="button" variant="plain" onClick={handleClose}>
{translate('::Cancel')}
</Button>
<Button type="submit" variant="solid" loading={isSubmitting}>
{translate('::Save')}
</Button>
</Dialog.Footer>
</Form>
)}

View file

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

View file

@ -94,7 +94,11 @@ function JsonRowOpDialogPane({
}
toast.push(
<Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
{translate(
data.index === -1
? '::App.Platform.RecordAdded'
: '::App.Platform.RecordUpdated',
)}
</Notification>,
{ placement: 'bottom-end' },
)
@ -102,7 +106,7 @@ function JsonRowOpDialogPane({
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -134,7 +138,7 @@ function JsonRowOpDialogPane({
<Input
{...field}
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)}
/>
)}
@ -150,7 +154,12 @@ function JsonRowOpDialogPane({
</FormItem>
</div>
<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')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
@ -166,21 +175,26 @@ function JsonRowOpDialogPane({
{data.operation === 'delete' && (
<>
<h5 className="mb-4">Delete</h5>
<p>Silmek istediğinize emin misiniz?</p>
<p>{translate('::DeleteConfirmation')}</p>
<Formik
initialValues={data}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
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',
})
},
)
handleClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
@ -194,7 +208,12 @@ function JsonRowOpDialogPane({
<Form>
<FormContainer size="sm">
<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')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { FaExclamationTriangle, FaUndo } from 'react-icons/fa'
import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
import type { OptionSpec } from './optionSpecs'
import { useLocalization } from '@/utils/hooks/useLocalization'
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'
@ -15,29 +16,52 @@ type OptionFieldProps = {
}
/** 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
const isObjectLike = typeof value === 'object'
if (spec.type === 'json') return undefined
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) {
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') {
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') {
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
}
const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
const current = value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : ''
const BooleanControl = ({
value,
onChange,
}: {
value: unknown
onChange: (v: unknown) => void
}) => {
const { translate } = useLocalization()
const current =
value === true || value === 'true'
? 'true'
: value === false || value === 'false'
? 'false'
: ''
return (
<select
@ -48,7 +72,7 @@ const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unk
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="false">false</option>
</select>
@ -64,6 +88,7 @@ const SelectControl = ({
value: unknown
onChange: (v: unknown) => void
}) => {
const { translate } = useLocalization()
const current = value === undefined || value === null ? '' : String(value)
const known = spec.choices?.some((choice) => String(choice.value) === current)
@ -78,18 +103,23 @@ const SelectControl = ({
onChange(choice ? choice.value : raw)
}}
>
<option value=""> tanımsız </option>
<option value="">{translate('::ListForms.EditorOptions.Undefined')}</option>
{spec.choices?.map((choice) => (
<option key={String(choice.value)} value={String(choice.value)}>
{choice.label}
</option>
))}
{current && !known && <option value={current}>{current} (mevcut değer)</option>}
{current && !known && (
<option value={current}>
{current} ({translate('::ListForms.EditorOptions.CurrentValue')})
</option>
)}
</select>
)
}
const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
const { translate } = useLocalization()
const serialized = value === undefined ? '' : JSON.stringify(value, null, 2)
const [draft, setDraft] = useState(serialized)
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>
)
}
@ -135,11 +169,14 @@ const StringListControl = ({
spec: OptionSpec
value: unknown
onChange: (v: unknown) => void
}) => (
}) => {
const { translate } = useLocalization()
return (
<input
className={controlClass}
value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
placeholder={spec.placeholder}
placeholder={spec.placeholder ? translate('::' + spec.placeholder) : undefined}
onChange={(event) => {
const items = event.target.value
.split(',')
@ -149,9 +186,13 @@ const StringListControl = ({
}}
/>
)
}
const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
const { translate } = useLocalization()
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
// 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.
@ -177,7 +218,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
className={controlClass}
type="number"
value={typeof value === 'number' ? value : leafToText(value)}
placeholder={spec.placeholder}
placeholder={placeholder}
onChange={(event) => onChange(coerceNumber(event.target.value))}
/>
)
@ -186,7 +227,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<input
className={controlClass}
value={leafToText(value)}
placeholder={spec.placeholder}
placeholder={placeholder}
onChange={(event) => onChange(coerceSize(event.target.value))}
/>
)
@ -195,8 +236,10 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<input
className={controlClass}
value={leafToText(value)}
placeholder={spec.placeholder}
onChange={(event) => onChange(event.target.value === '' ? undefined : event.target.value)}
placeholder={placeholder}
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">
<span
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}
</span>
{spec.platform && (
<span
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
</span>
@ -229,7 +272,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
<button
type="button"
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)}
>
<FaUndo />
@ -240,13 +283,13 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
{mismatch && (
<p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600">
<FaExclamationTriangle className="mt-[2px] shrink-0" />
<span>{mismatch}</span>
<span>{translate(mismatch.key, mismatch.params)}</span>
</p>
)}
{/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */}
{!mismatch && spec.help && (
<p className="mt-1 truncate text-[11px] text-gray-400" title={spec.help}>
{spec.help}
{!mismatch && help && (
<p className="mt-1 truncate text-[11px] text-gray-400" title={help}>
{help}
</p>
)}
</div>

View file

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

View file

@ -57,41 +57,68 @@ export type OptionGroupKey =
export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [
{
key: 'common',
title: 'Genel Davranış',
description: 'Tüm editörlerde geçerli olan durum, yetki ve doğrulama ayarları.',
title: 'ListForms.EditorOptions.Spec.Common.Title',
description: 'ListForms.EditorOptions.Spec.Common.Description',
},
{
key: 'appearance',
title: 'Görünüm ve Boyut',
description: 'Etiket, boyut ve stil ayarları. Boyutlar sayı veya 100% gibi CSS değeri olabilir.',
title: 'ListForms.EditorOptions.Spec.Appearance.Title',
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',
title: 'Açılır Liste / Arama',
description: 'SelectBox, Lookup, TagBox ve GridBox için ortak liste ve arama ayarları.',
title: 'ListForms.EditorOptions.Spec.Dropdown.Title',
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',
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',
title: 'Görsel Yükleme / Önizleme',
description: 'ImageUpload ve ImageViewer editörleri için yükleme ve küçük resim ayarları.',
title: 'ListForms.EditorOptions.Spec.Image.Title',
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',
title: 'Liste Sütunu Etkisi',
description:
'Bu alanlar düzenleme editörünün yanı sıra listedeki sütun gösterimini de etkiler.',
title: 'ListForms.EditorOptions.Spec.Grid.Title',
description: 'ListForms.EditorOptions.Spec.Grid.Description',
},
]
@ -115,8 +142,6 @@ const SLIDER_EDITORS = ['dxSlider', 'dxRangeSlider']
/** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */
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üğü
* ------------------------------------------------------------------ */
@ -128,31 +153,43 @@ export const optionSpecs: OptionSpec[] = [
label: 'readOnly',
type: 'boolean',
group: 'common',
help: 'Alan görünür ama değiştirilemez. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.ReadOnly.Help',
},
{
path: 'disabled',
label: 'disabled',
type: 'boolean',
group: 'common',
help: 'Alan pasifleşir ve forma dahil edilmez. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.Disabled.Help',
},
{
path: 'visible',
label: 'visible',
type: 'boolean',
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',
label: 'valueChangeEvent',
type: 'text',
group: 'common',
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',
@ -183,14 +220,14 @@ export const optionSpecs: OptionSpec[] = [
type: 'text',
group: 'common',
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',
label: 'inputAttr.aria-label',
type: 'text',
group: 'common',
help: 'Erişilebilirlik etiketi.',
help: 'ListForms.EditorOptions.Spec.InputAttrAriaLabel.Help',
},
{
path: 'inputAttr.style',
@ -198,7 +235,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'text',
group: 'common',
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 ──────────────────────────────────────────────────────
@ -208,7 +245,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'size',
group: 'appearance',
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' },
{
@ -216,9 +253,15 @@ export const optionSpecs: OptionSpec[] = [
label: 'placeholder',
type: 'text',
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',
label: 'labelMode',
@ -247,7 +290,7 @@ export const optionSpecs: OptionSpec[] = [
label: 'showClearButton',
type: 'boolean',
group: 'appearance',
help: 'Temizleme (x) butonu. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.ShowClearButton.Help',
},
// ── Metin / Maske ────────────────────────────────────────────────
@ -275,7 +318,14 @@ export const optionSpecs: OptionSpec[] = [
editors: TEXT_EDITORS,
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',
label: 'maskInvalidMessage',
@ -300,7 +350,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean',
group: 'text',
editors: TEXT_EDITORS,
help: 'true ise maske karakterleri de veritabanına yazılır. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.UseMaskedValue.Help',
},
{
path: 'maskRules.X',
@ -309,16 +359,22 @@ export const optionSpecs: OptionSpec[] = [
group: 'text',
editors: TEXT_EDITORS,
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',
label: 'autoResizeEnabled',
type: 'boolean',
group: 'text',
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: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
@ -347,7 +403,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'select',
group: 'number',
editors: ['dxNumberBox'],
help: 'Sayısal format tipi. Seçilirse format alanı nesne olur.',
help: 'ListForms.EditorOptions.Spec.FormatType.Help',
choices: [
{ value: 'fixedPoint', label: 'fixedPoint' },
{ value: 'decimal', label: 'decimal' },
@ -366,7 +422,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'number',
group: 'number',
editors: ['dxNumberBox'],
help: 'Ondalık basamak sayısı.',
help: 'ListForms.EditorOptions.Spec.FormatPrecision.Help',
},
{
path: 'format.currency',
@ -389,7 +445,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean',
group: 'number',
editors: ['dxNumberBox', 'dxDateBox'],
help: 'Girdiyi formatlı maske olarak yönetir. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.UseMaskBehavior.Help',
},
// ── Tarih ────────────────────────────────────────────────────────
@ -425,7 +481,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'date',
editors: DATE_EDITORS,
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',
@ -434,7 +490,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'date',
editors: DATE_EDITORS,
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',
@ -442,11 +498,11 @@ export const optionSpecs: OptionSpec[] = [
type: 'number',
group: 'date',
editors: DATE_EDITORS,
help: 'Saat listesinde dakika aralığı.',
help: 'ListForms.EditorOptions.Spec.Interval.Help',
},
{
path: 'min',
label: 'min (tarih)',
label: 'min',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
@ -454,7 +510,7 @@ export const optionSpecs: OptionSpec[] = [
},
{
path: 'max',
label: 'max (tarih)',
label: 'max',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
@ -515,7 +571,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown',
editors: DROPDOWN_EDITORS,
platform: true,
help: 'Listede arama kutusu. TagBox için backend TagBoxOptions olarak da okur. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.SearchEnabled.Help',
},
{
path: 'searchMode',
@ -535,7 +591,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown',
editors: DROPDOWN_EDITORS,
placeholder: 'name',
help: 'Aramanın hangi alanda yapılacağı.',
help: 'ListForms.EditorOptions.Spec.SearchExpr.Help',
},
{
path: 'searchTimeout',
@ -565,7 +621,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'dropdown',
editors: DROPDOWN_EDITORS,
platform: true,
help: 'Listede olmayan değerin yazılmasına izin verir. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.AcceptCustomValue.Help',
},
{
path: 'noDataText',
@ -573,7 +629,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'text',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
placeholder: 'Kayıt bulunamadı',
placeholder: 'ListForms.EditorOptions.Spec.NoDataText.Placeholder',
},
{
path: 'deferRendering',
@ -581,7 +637,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
help: 'Liste yalnızca açıldığında render edilir. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.DeferRendering.Help',
},
{
path: 'wrapItemText',
@ -620,7 +676,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'tagBox',
editors: ['dxTagBox'],
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',
@ -645,13 +701,19 @@ export const optionSpecs: OptionSpec[] = [
group: 'tagBox',
editors: ['dxTagBox'],
platform: true,
help: 'Boş bırakılırsa platform varsayılanı useButtons.',
help: 'ListForms.EditorOptions.Spec.ApplyValueMode.Help',
choices: [
{ value: 'instantly', label: 'instantly' },
{ 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',
label: 'hideSelectedItems',
@ -669,7 +731,7 @@ export const optionSpecs: OptionSpec[] = [
editors: ['dxGridBox'],
platform: true,
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',
@ -678,7 +740,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'gridBox',
editors: ['dxGridBox'],
platform: true,
help: 'multiple seçilirse liste sütunu da çoklu değer olarak çalışır.',
help: 'ListForms.EditorOptions.Spec.SelectionMode.Help',
choices: [
{ value: 'single', label: 'single' },
{ value: 'multiple', label: 'multiple' },
@ -692,7 +754,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'gridBox',
editors: ['dxGridBox'],
platform: true,
help: 'ılır tabloda filtre satırı. ' + booleanHelp,
help: 'ListForms.EditorOptions.Spec.FilterRowVisible.Help',
},
// ── Görsel ───────────────────────────────────────────────────────
@ -703,7 +765,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image',
editors: IMAGE_EDITORS,
platform: true,
help: 'Boş bırakılırsa görsel base64 olarak saklanır.',
help: 'ListForms.EditorOptions.Spec.UploadUrl.Help',
},
{
path: 'fileFieldName',
@ -712,7 +774,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image',
editors: IMAGE_EDITORS,
placeholder: 'file',
help: 'Yükleme isteğindeki form alan adı.',
help: 'ListForms.EditorOptions.Spec.FileFieldName.Help',
},
{
path: 'accept',
@ -730,7 +792,7 @@ export const optionSpecs: OptionSpec[] = [
group: 'image',
editors: IMAGE_EDITORS,
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',
@ -748,10 +810,22 @@ export const optionSpecs: OptionSpec[] = [
type: 'text',
group: 'choice',
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',
label: 'layout',
@ -794,7 +868,13 @@ export const optionSpecs: OptionSpec[] = [
{ 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 ──────────────────────────────────────────────────
{
@ -821,7 +901,7 @@ export const optionSpecs: OptionSpec[] = [
type: 'json',
group: 'html',
editors: ['dxHtmlEditor'],
help: 'Araç çubuğu tanımı. Hazır ayarlardaki "htmlEditor toolbar" bunu doldurur.',
help: 'ListForms.EditorOptions.Spec.ToolbarItems.Help',
},
{
path: 'mediaResizing.enabled',
@ -857,21 +937,21 @@ export const optionSpecs: OptionSpec[] = [
type: 'text',
group: 'grid',
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',
label: 'encodeHtml',
type: 'boolean',
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',
label: 'buttons',
type: 'json',
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',
label: 'Salt okunur',
description: 'readOnly: true — alan görünür ama değiştirilemez.',
description: 'ListForms.EditorOptions.Preset.ReadOnly.Description',
value: { readOnly: true },
},
{
key: 'disabled',
label: 'Pasif',
description: 'disabled: true — alan tamamen pasifleşir.',
description: 'ListForms.EditorOptions.Preset.Disabled.Description',
value: { disabled: true },
},
{
key: 'rightAligned',
label: 'Sağa yaslı',
description: 'Girdi metnini sağa yaslar. Tutar/miktar alanları için.',
label: 'ListForms.EditorOptions.Preset.RightAligned.Label',
description: 'ListForms.EditorOptions.Preset.RightAligned.Description',
value: { inputAttr: { style: 'text-align: right' } },
},
{
key: 'fixedPoint2',
label: 'Ondalık (2 hane)',
label: 'ListForms.EditorOptions.Preset.FixedPoint2.Label',
description: 'format.type fixedPoint, precision 2.',
editors: ['dxNumberBox'],
value: { format: { type: 'fixedPoint', precision: 2 } },
},
{
key: 'numberSpin2',
label: 'Ondalık + spin',
description: 'precision 2, mask davranışı ve artır/azalt butonları.',
label: 'ListForms.EditorOptions.Preset.NumberSpin2.Label',
description: 'ListForms.EditorOptions.Preset.NumberSpin2.Description',
editors: ['dxNumberBox'],
value: {
format: { type: 'fixedPoint', precision: 2 },
@ -132,14 +132,14 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
{
key: 'currency',
label: 'Para birimi (TRY)',
description: 'currency formatı, 2 ondalık.',
description: 'ListForms.EditorOptions.Preset.Currency.Description',
editors: ['dxNumberBox'],
value: { format: { type: 'currency', precision: 2, currency: 'TRY' } },
},
{
key: 'date',
label: 'Tarih dd/MM/yyyy',
description: 'Görünüm ve veritabanı biçimini birlikte ayarlar.',
label: 'ListForms.EditorOptions.Preset.DateFormat.Label',
description: 'ListForms.EditorOptions.Preset.Date.Description',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: {
type: 'date',
@ -149,8 +149,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'dateTime',
label: 'Tarih + saat',
description: 'dd/MM/yyyy HH:mm görünümü, ISO serileştirme.',
label: 'ListForms.EditorOptions.Preset.DateTime.Label',
description: 'ListForms.EditorOptions.Preset.DateTime.Description',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: {
type: 'datetime',
@ -160,8 +160,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'timePicker',
label: 'Saat seçici',
description: '5 dakika aralıklı liste tipi saat seçimi.',
label: 'ListForms.EditorOptions.Preset.TimePicker.Label',
description: 'ListForms.EditorOptions.Preset.TimePicker.Description',
editors: ['dxDateBox'],
value: {
type: 'time',
@ -175,7 +175,7 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
{
key: 'phoneMask',
label: 'Telefon maskesi',
description: 'Maske, yer tutucu ve hata mesajı.',
description: 'ListForms.EditorOptions.Preset.PhoneMask.Description',
editors: ['dxTextBox', 'dxAutocomplete'],
value: {
mask: '(000) 000-0000',
@ -187,22 +187,34 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'textAreaAuto',
label: 'Otomatik büyüyen alan',
description: 'autoResizeEnabled ve minimum yükseklik.',
label: 'ListForms.EditorOptions.Preset.TextAreaAuto.Label',
description: 'ListForms.EditorOptions.Preset.TextAreaAuto.Description',
editors: ['dxTextArea'],
value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 },
},
{
key: 'searchableList',
label: 'Aranabilir liste',
description: 'İçinde geçen kayıtları arar, 300 ms gecikme.',
editors: ['dxSelectBox', 'dxLookup', 'dxTagBox', 'dxDropDownBox', 'dxGridBox', 'dxAutocomplete'],
value: { searchEnabled: true, searchMode: 'contains', searchTimeout: 300, showClearButton: true },
description: 'ListForms.EditorOptions.Preset.SearchableList.Description',
editors: [
'dxSelectBox',
'dxLookup',
'dxTagBox',
'dxDropDownBox',
'dxGridBox',
'dxAutocomplete',
],
value: {
searchEnabled: true,
searchMode: 'contains',
searchTimeout: 300,
showClearButton: true,
},
},
{
key: 'tagBoxInstant',
label: 'TagBox anlık seçim',
description: 'Onay kutulu, butonsuz, anında uygulanan çoklu seçim.',
label: 'ListForms.EditorOptions.Preset.TagBoxInstant.Label',
description: 'ListForms.EditorOptions.Preset.TagBoxInstant.Description',
editors: ['dxTagBox'],
value: {
showSelectionControls: true,
@ -213,8 +225,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'gridBoxSingle',
label: 'GridBox tekli seçim',
description: 'key/name sütunları, filtre satırıık, tek seçim.',
label: 'ListForms.EditorOptions.Preset.GridBoxSingle.Label',
description: 'ListForms.EditorOptions.Preset.GridBoxSingle.Description',
editors: ['dxGridBox'],
value: {
columns: ['key', 'name'],
@ -227,8 +239,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'gridBoxMulti',
label: 'GridBox çoklu seçim',
description: 'Çoklu seçim; liste sütunu da çoklu değer olarak çalışır.',
label: 'ListForms.EditorOptions.Preset.GridBoxMulti.Label',
description: 'ListForms.EditorOptions.Preset.GridBoxMulti.Description',
editors: ['dxGridBox'],
value: {
columns: ['key', 'name'],
@ -240,27 +252,27 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
},
{
key: 'imageMulti',
label: 'Çoklu görsel 80x80',
description: 'Çoklu yükleme ve 80x80 küçük resim.',
label: 'ListForms.EditorOptions.Preset.ImageMulti.Label',
description: 'ListForms.EditorOptions.Preset.ImageMulti.Description',
editors: ['dxImageUpload', 'dxImageViewer'],
value: { width: 80, height: 80, multiple: true, accept: 'image/*' },
},
{
key: 'htmlToolbar',
label: 'HtmlEditor araç çubuğu',
description: 'Tam araç çubuğu, görsel yükleme ve boyutlandırma.',
label: 'ListForms.EditorOptions.Preset.HtmlToolbar.Label',
description: 'ListForms.EditorOptions.Preset.HtmlToolbar.Description',
editors: ['dxHtmlEditor'],
value: buildHtmlEditorOptions(),
},
{
key: 'height100',
label: 'Yükseklik 100',
label: 'ListForms.EditorOptions.Preset.Height100.Label',
description: 'height: 100',
value: { height: 100 },
},
{
key: 'height200',
label: 'Yükseklik 200',
label: 'ListForms.EditorOptions.Preset.Height200.Label',
description: 'height: 200',
value: { height: 200 },
},

View file

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

View file

@ -34,13 +34,17 @@ export type RuleCondition = {
export type RuleTrigger = 'change' | 'open' | 'both'
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',
label: 'Form açılırken',
help: 'Popup edit form açıldığında bir kez çalışır. Varsayılan değer üretmek için.',
label: 'ListForms.ScriptBuilder.Open.Label',
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. */
@ -93,7 +97,7 @@ export type Recipe = {
conditionIsArgument?: boolean
params: RecipeParam[]
build: (rule: ScriptRule) => string
describe: (rule: ScriptRule) => string
describe: (rule: ScriptRule, t: TranslateFn) => string
example: string
}
@ -117,17 +121,31 @@ export type RecipeId =
| 'custom'
export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [
{ key: 'calc', title: 'Hesaplama', description: 'Alanlardan yeni bir değer üretir.' },
{ key: 'data', title: 'Veri', description: 'Alanlara değer yazar, kopyalar veya temizler.' },
{ 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: 'integration', title: 'Entegrasyon', description: 'API ve servis çağrıları, serbest kod.' },
{ key: 'calc', title: 'Hesaplama', description: 'ListForms.ScriptBuilder.Calc.Description' },
{ key: 'data', title: 'Veri', description: 'ListForms.ScriptBuilder.Data.Description' },
{
key: 'view',
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
* ------------------------------------------------------------------ */
export type TranslateFn = (key: string, params?: Record<string, string | number>) => string
const q = (input: string) => JSON.stringify(input ?? '')
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' ? ' || ' : ' && ')
}
export const describeCondition = (condition: RuleCondition): string => {
if (condition.operator === 'always' || !condition.source) return 'Her zaman'
const source = label(condition.source, 'alan')
export const describeCondition = (condition: RuleCondition, t: TranslateFn): string => {
if (condition.operator === 'always' || !condition.source) {
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) {
case 'empty':
return `${source} boşsa`
return t('::ListForms.ScriptBuilder.Describe.Empty', { source })
case 'notEmpty':
return `${source} doluysa`
return t('::ListForms.ScriptBuilder.Describe.NotEmpty', { source })
case 'isTrue':
return `${source} işaretliyse`
return t('::ListForms.ScriptBuilder.Describe.IsTrue', { source })
case 'isFalse':
return `${source} işaretli değilse`
return t('::ListForms.ScriptBuilder.Describe.IsFalse', { source })
case 'contains':
return `${source} içinde "${label(condition.value, 'değer')}" geçiyorsa`
return t('::ListForms.ScriptBuilder.Describe.Contains', { source, value })
case 'greaterThan':
return `${source} > ${label(condition.value, 'değer')} ise`
return t('::ListForms.ScriptBuilder.Describe.GreaterThan', { source, value })
case 'lessThan':
return `${source} < ${label(condition.value, 'değer')} ise`
return t('::ListForms.ScriptBuilder.Describe.LessThan', { source, value })
case 'notEquals':
return `${source} ≠ "${label(condition.value, 'değer')}" ise`
return t('::ListForms.ScriptBuilder.Describe.NotEquals', { source, value })
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[] = [
{
id: 'multiply',
label: 'Çarpım (miktar × fiyat)',
label: 'ListForms.ScriptBuilder.Multiply.Label',
group: 'calc',
summary: 'İki alanı çarpar, yuvarlar ve hedefe yazar.',
summary: 'ListForms.ScriptBuilder.Multiply.Summary',
example: "set('TotalAmount', round(num('Quantity') * num('UnitPrice'), 2))",
params: [
{ key: 'left', label: 'Birinci alan', type: 'field' },
{ key: 'right', label: 'İkinci alan', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'right', label: 'ListForms.ScriptBuilder.Right.Label', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label', type: 'field' },
{
key: 'digits',
label: 'Ondalık',
label: 'ListForms.ScriptBuilder.Digits.Label',
type: 'number',
optional: true,
placeholder: '2',
help: 'Boş bırakılırsa 2 kullanılır.',
help: 'ListForms.ScriptBuilder.Digits.Help',
},
],
build: ({ params }) =>
`set(${q(params.target)}, round(${numberOrField(params.left)} * ${numberOrField(
params.right,
)}, ${params.digits || 2}))`,
describe: ({ params }) =>
`${label(params.left, 'alan')} × ${label(params.right, 'alan')} sonucunu ${label(
params.target,
'hedef',
)} alanına yaz.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Multiply', {
left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'subtract',
label: 'Fark (brüt indirim)',
label: 'ListForms.ScriptBuilder.Subtract.Label',
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))",
params: [
{ key: 'left', label: ıkarılan alan', type: 'field' },
{ key: 'right', label: ıkan alan', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
{ key: 'left', label: 'ListForms.ScriptBuilder.Left.Label', type: 'field' },
{ key: 'right', label: 'ListForms.ScriptBuilder.Right.Label2', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{
key: 'digits',
label: 'ListForms.ScriptBuilder.Digits.Label',
type: 'number',
optional: true,
placeholder: '2',
},
],
build: ({ params }) =>
`set(${q(params.target)}, round(${numberOrField(params.left)} - ${numberOrField(
params.right,
)}, ${params.digits || 2}))`,
describe: ({ params }) =>
`${label(params.left, 'alan')} ${label(params.right, 'alan')} sonucunu ${label(
params.target,
'hedef',
)} alanına yaz.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Subtract', {
left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'percent',
label: 'Yüzde (KDV, iskonto)',
label: 'ListForms.ScriptBuilder.Percent.Label',
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))",
params: [
{ key: 'amount', label: 'Tutar alanı', type: 'field' },
{ key: 'amount', label: 'ListForms.ScriptBuilder.Amount.Label', type: 'field' },
{
key: 'rate',
label: 'Oran',
type: 'text',
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',
label: 'Sonuç',
label: 'ListForms.ScriptBuilder.Mode.Label',
type: 'select',
optional: true,
choices: [
{ value: '', label: 'Yalnızca yüzde tutarı' },
{ value: 'add', label: 'Tutar + yüzde' },
{ value: 'subtract', label: 'Tutar yüzde' },
{ value: '', label: 'ListForms.ScriptBuilder.X.Label' },
{ value: 'add', label: 'ListForms.ScriptBuilder.Add.Label' },
{ value: 'subtract', label: 'ListForms.ScriptBuilder.Subtract.Label' },
],
},
],
@ -295,191 +330,215 @@ export const recipes: Recipe[] = [
: portion
return `set(${q(params.target)}, round(${expression}, ${params.digits || 2}))`
},
describe: ({ params }) =>
`${label(params.amount, 'tutar')} üzerinden %${label(params.rate, 'oran')} hesabını ${label(
params.target,
'hedef',
)} alanına yaz.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Percent', {
amount: label(params.amount, t('::ListForms.ScriptBuilder.Word.Amount')),
rate: label(params.rate, t('::ListForms.ScriptBuilder.Word.Rate')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'today',
label: 'Bugünün tarihi',
label: 'ListForms.ScriptBuilder.Today.Label',
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))",
params: [
{ key: 'target', label: 'Hedef alan', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{
key: 'offset',
label: 'Gün ekle',
label: 'ListForms.ScriptBuilder.Offset.Label',
type: 'number',
optional: true,
placeholder: '0',
help: 'Negatif yazarsan geçmiş bir tarih üretir.',
help: 'ListForms.ScriptBuilder.Offset.Help',
},
],
build: ({ params }) => {
const offset = Number(params.offset) || 0
const base = offset
? `new Date(Date.now() + ${offset} * 86400000)`
: 'new Date()'
const base = offset ? `new Date(Date.now() + ${offset} * 86400000)` : 'new Date()'
return `set(${q(params.target)}, ${base}.toISOString().slice(0, 10))`
},
describe: ({ params }) => {
describe: ({ params }, t) => {
const offset = Number(params.offset) || 0
const when = offset > 0 ? `${offset} gün sonrasını` : offset < 0 ? `${-offset} gün öncesini` : 'bugünü'
return `${when} ${label(params.target, 'hedef')} alanına yaz.`
const when =
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',
label: 'Toplama',
group: 'calc',
summary: 'Seçilen alanları toplar ve hedefe yazar.',
summary: 'ListForms.ScriptBuilder.Sum.Summary',
example: "set('Total', round(sum('Net', 'Tax'), 2))",
params: [
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'sources', label: 'Toplanacak alanlar', type: 'fieldList' },
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label4', type: 'field' },
{ key: 'sources', label: 'ListForms.ScriptBuilder.Sources.Label3', type: 'fieldList' },
{
key: 'digits',
label: 'ListForms.ScriptBuilder.Digits.Label3',
type: 'number',
optional: true,
placeholder: '2',
},
],
build: ({ params, fields }) =>
`set(${q(params.target)}, round(sum(${(fields ?? []).map(q).join(', ')}), ${
params.digits || 2
}))`,
describe: ({ params, fields }) =>
`${(fields ?? []).join(' + ') || 'alanlar'} toplamını ${label(
params.target,
'hedef',
)} alanına yaz.`,
describe: ({ params, fields }, t) =>
t('::ListForms.ScriptBuilder.Describe.Sum', {
sources: (fields ?? []).join(' + ') || t('::ListForms.ScriptBuilder.Word.Fields'),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'formula',
label: 'Serbest formül',
label: 'ListForms.ScriptBuilder.Formula.Label',
group: 'calc',
summary: 'JavaScript ifadesi yazarsın; num/str/get/selected kullanabilirsin.',
summary: 'ListForms.ScriptBuilder.Formula.Summary',
example: "set('Net', num('Gross') - num('Discount'))",
params: [
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label5', type: 'field' },
{
key: 'expression',
label: 'İfade',
label: 'ListForms.ScriptBuilder.Expression.Label',
type: 'text',
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'})`,
describe: ({ params }) =>
`${label(params.expression, 'ifade')} sonucunu ${label(params.target, 'hedef')} alanına yaz.`,
describe: ({ params }, t) =>
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',
label: 'Gün farkı',
label: 'ListForms.ScriptBuilder.Days.Label',
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'))",
params: [
{ key: 'start', label: 'Başlangıç tarihi', type: 'field' },
{ key: 'end', label: 'Bitiş tarihi', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'start', label: 'ListForms.ScriptBuilder.Start.Label', type: 'field' },
{ key: 'end', label: 'ListForms.ScriptBuilder.End.Label', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label6', type: 'field' },
],
build: ({ params }) =>
`set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`,
describe: ({ params }) =>
`${label(params.start, 'başlangıç')}${label(params.end, 'bitiş')} gün farkını ${label(
params.target,
'hedef',
)} alanına yaz.`,
build: ({ params }) => `set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Days', {
start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'hours',
label: 'Saat farkı',
label: 'ListForms.ScriptBuilder.Hours.Label',
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'))",
params: [
{ key: 'start', label: 'Başlangıç saati', type: 'field' },
{ key: 'end', label: 'Bitiş saati', type: 'field' },
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
{ key: 'start', label: 'ListForms.ScriptBuilder.Start.Label2', type: 'field' },
{ key: 'end', label: 'ListForms.ScriptBuilder.End.Label2', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label7', type: 'field' },
],
build: ({ params }) =>
`set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`,
describe: ({ params }) =>
`${label(params.start, 'başlangıç')}${label(params.end, 'bitiş')} saat farkını ${label(
params.target,
'hedef',
)} alanına yaz.`,
build: ({ params }) => `set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Hours', {
start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'copy',
label: 'Seçili kayıttan kopyala',
label: 'ListForms.ScriptBuilder.Copy.Label',
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')",
params: [
{
key: 'source',
label: 'Seçili kayıttaki kolon/yol',
label: 'ListForms.ScriptBuilder.Source.Label',
type: 'text',
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)})`,
describe: ({ params }) =>
`Seçili kayıttaki ${label(params.source, 'kolon')} değerini ${label(
params.target,
'hedef',
)} alanına kopyala.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Copy', {
source: label(params.source, t('::ListForms.ScriptBuilder.Word.Column')),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'setValue',
label: 'Değer yaz',
label: 'ListForms.ScriptBuilder.SetValue.Label',
group: 'data',
summary: 'Sabit metin veya {Alan} / {value} / {selected.Yol} tokenları yazar.',
summary: 'ListForms.ScriptBuilder.SetValue.Summary',
example: "set('Aciklama', tpl('{Musteri} - {value}'))",
params: [
{ key: 'target', label: 'Hedef alan', type: 'field' },
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
{
key: 'text',
label: 'Değer',
label: 'ListForms.ScriptBuilder.Text.Label',
type: 'text',
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)}))`,
describe: ({ params }) =>
`${label(params.target, 'hedef')} alanına "${label(params.text, 'değer')}" yaz.`,
describe: ({ params }, t) =>
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',
label: 'Alanları temizle',
label: 'ListForms.ScriptBuilder.Clear.Label',
group: 'data',
summary: 'Seçilen alanları null yapar.',
summary: 'ListForms.ScriptBuilder.Clear.Summary',
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(', ')})`,
describe: ({ fields }) => `${(fields ?? []).join(', ') || 'alanları'} temizle.`,
describe: ({ fields }, t) =>
t('::ListForms.ScriptBuilder.Describe.Clear', {
sources: (fields ?? []).join(', ') || t('::ListForms.ScriptBuilder.Word.Fields'),
}),
},
{
id: 'readOnly',
label: 'Salt okunur yap',
group: 'view',
summary: 'Koşul sağlandığında alanı kilitler, sağlanmadığında açar.',
summary: 'ListForms.ScriptBuilder.ReadOnly.Summary',
conditionIsArgument: true,
example: "readOnly('SatisSebebi', str('Durum') === 'Aktif')",
params: [
{ key: 'target', label: 'Kilitlenecek alan', type: 'field' },
{
key: 'invert',
label: 'Tersine çevir',
label: 'ListForms.ScriptBuilder.Invert.Label',
type: 'select',
optional: true,
help: 'Evet seçilirse koşul sağlandığında alan açılır.',
help: 'ListForms.ScriptBuilder.Invert.Help',
choices: [
{ value: '', label: 'Hayır' },
{ value: 'true', label: 'Evet' },
{ value: '', label: 'ListForms.ScriptBuilder.X.Label2' },
{ value: 'true', label: 'ListForms.ScriptBuilder.Yes' },
],
},
],
@ -488,39 +547,62 @@ export const recipes: Recipe[] = [
const flag = rule.params.invert === 'true' ? `!(${expression})` : expression
return `readOnly(${q(rule.params.target)}, ${flag})`
},
describe: ({ params, condition }) =>
`${describeCondition(condition)} ${label(params.target, 'alan')} alanını ${
params.invert === 'true' ? 'düzenlenebilir' : 'salt okunur'
} yap.`,
describe: ({ params, condition }, t) =>
t('::ListForms.ScriptBuilder.Describe.ReadOnly', {
condition: describeCondition(condition, t),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Field')),
mode: t(
params.invert === 'true'
? '::ListForms.ScriptBuilder.Word.Editable'
: '::ListForms.ScriptBuilder.Word.ReadOnly',
),
}),
},
{
id: 'notify',
label: 'Uyarı göster',
label: 'ListForms.ScriptBuilder.Notify.Label',
group: 'interaction',
summary: 'Kullanıcıya bilgi mesajı gösterir.',
summary: 'ListForms.ScriptBuilder.Notify.Summary',
example: "notify('Limit aşıldı')",
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)})`,
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',
label: 'Onay iste',
label: 'ListForms.ScriptBuilder.Ask.Label',
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",
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`,
describe: ({ params }) =>
`"${label(params.message, 'mesaj')}" onayını iste; vazgeçilirse değeri geri al.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.Ask', {
message: label(params.message, t('::ListForms.ScriptBuilder.Word.Message')),
}),
},
{
id: 'openUrl',
label: 'URL aç',
label: 'ListForms.ScriptBuilder.OpenUrl.Label',
group: 'interaction',
summary: 'Yeni sekmede veya aynı sekmede adres açar.',
summary: 'ListForms.ScriptBuilder.OpenUrl.Summary',
example: "openUrl('/report?id={Id}')",
params: [
{ key: 'url', label: 'URL', type: 'text', placeholder: '/report?id={Id}' },
@ -539,16 +621,19 @@ export const recipes: Recipe[] = [
params.target && params.target !== '_blank'
? `openUrl(${q(params.url)}, ${q(params.target)})`
: `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',
label: 'API sonucunu alana yaz',
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'))",
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: 'path',
@ -556,24 +641,24 @@ export const recipes: Recipe[] = [
type: 'text',
optional: true,
placeholder: 'data.name',
help: 'Boş bırakılırsa tüm JSON yazılır.',
help: 'ListForms.ScriptBuilder.Path.Help',
},
],
build: ({ params }) =>
`set(${q(params.target)}, await api(${q(params.url)}${
params.path ? `, ${q(params.path)}` : ''
}))`,
describe: ({ params }) =>
`${label(params.url, 'api')} çağrısının sonucunu ${label(
params.target,
'hedef',
)} alanına yaz.`,
describe: ({ params }, t) =>
t('::ListForms.ScriptBuilder.Describe.ApiToField', {
url: label(params.url, 'api'),
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
}),
},
{
id: 'custom',
label: 'Serbest tek satır',
label: 'ListForms.ScriptBuilder.Custom.Label',
group: 'integration',
summary: 'Servis çağrısı veya kendi yazdığın tek satırlık kod.',
summary: 'ListForms.ScriptBuilder.Custom.Summary',
example: 'UiEvalService.ApiGenerateBackgroundWorkers()',
params: [
{
@ -584,7 +669,10 @@ export const recipes: Recipe[] = [
},
],
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. */
export const runtimeApiReference: { signature: string; description: string }[] = [
{ signature: 'value / field', description: 'Değişen alanın değeri ve adı.' },
{ signature: "get('Alan')", description: 'Form değerini okur (büyük/küçük harf duyarsız).' },
{ 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: "set('Alan', deger)", description: 'Alana yazar. Nesne de verebilirsin.' },
{ signature: "set({ A: 1, B: 2 })", description: 'Birden fazla alanı tek çağrıda yazar.' },
{ signature: "clear('A', 'B')", description: 'Alanları null yapar.' },
{ signature: "copy('kolon', 'Alan')", description: 'Seçili lookup kaydından kopyalar.' },
{ signature: "selected('Yol') / snum() / sstr()", description: 'Seçili kaydı okur.' },
{ signature: "readOnly('Alan', kosul)", description: 'Alanı kilitler veya açar.' },
{ signature: "round(x, 2) / sum('A','B')", description: 'Yuvarlama ve toplama.' },
{ signature: "days('Bas','Bit') / hours('Bas','Bit')", description: 'Tarih ve saat farkı.' },
{ signature: "tpl('{Alan} - {value}')", description: 'Token doldurur.' },
{ signature: "notify('mesaj') / ask('mesaj')", description: 'Uyarı ve onay. ask false ise geri alır.' },
{ signature: "openUrl('/adres')", description: 'Yeni sekmede açar.' },
{ signature: "await api('/api/x', 'data.name')", description: 'JSON çağrısı yapar.' },
{ signature: 'value / field', description: 'ListForms.ScriptBuilder.Api.ValueField' },
{ signature: "get('Alan')", description: 'ListForms.ScriptBuilder.Api.Get' },
{
signature: "num('Alan') / str('Alan') / bool('Alan')",
description: 'ListForms.ScriptBuilder.Api.Cast',
},
{ signature: "dateOf('Alan')", description: 'ListForms.ScriptBuilder.Api.DateOf' },
{ signature: "set('Alan', deger)", description: 'ListForms.ScriptBuilder.Api.Set' },
{ signature: 'set({ A: 1, B: 2 })', description: 'ListForms.ScriptBuilder.Api.SetMany' },
{ signature: "clear('A', 'B')", description: 'ListForms.ScriptBuilder.Api.Clear' },
{ signature: "copy('kolon', 'Alan')", description: 'ListForms.ScriptBuilder.Api.Copy' },
{
signature: "selected('Yol') / snum() / sstr()",
description: 'ListForms.ScriptBuilder.Api.Selected',
},
{ 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',
description:
'Form açılışında true, değer değişiminde false. Script açılışta da çalışsın istiyorsan bu ifadeyi kullan.',
description: 'ListForms.ScriptBuilder.Api.IsReady',
},
]

View file

@ -618,7 +618,7 @@ const Wizard = () => {
} catch {
toast.push(
<Notification type="danger">
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard file.'}
{translate('::App.Listforms.WizardFileLoadError')}
</Notification>,
{ placement: 'bottom-end' },
)
@ -789,7 +789,7 @@ const Wizard = () => {
const handleDeploy = async () => {
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
@ -861,11 +861,13 @@ const Wizard = () => {
return (
<Container>
<PageTitle title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))} />
<PageTitle
title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))}
/>
{isLoadingEditData && (
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">
{translate('::App.Loading') || 'Loading...'}
{translate('::App.Loading')}
</p>
)}
@ -879,32 +881,32 @@ const Wizard = () => {
>
{({ touched, errors, values }) => {
const activeSteps = [
{ id: 0, title: translate('::ListForms.Wizard.MenuInfo') || 'Menu Info' },
{ id: 0, title: translate('::ListForms.Wizard.MenuInfo') },
{
id: 1,
title: translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings',
title: translate('::ListForms.Wizard.ListFormSettings'),
},
{
id: 2,
title: translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields',
title: translate('::ListForms.Wizard.ListFormFields'),
},
{
id: 3,
title: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
title: translate('::ListForms.ListFormEdit.SubForms'),
},
{
id: 4,
title: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
title: translate('::ListForms.ListFormEdit.TabWidgets'),
},
{
id: 5,
title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
title: translate('::ListForms.ListFormEdit.TabWorkflow'),
},
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
id: step.id,
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
@ -931,7 +933,7 @@ const Wizard = () => {
<Steps.Item
customIcon={<FaFolderOpen />}
status="pending"
title={translate('::App.Listforms.WizardManager') || 'Wizard Manager'}
title={translate('::App.Listforms.WizardManager')}
onStepChange={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager)
}

View file

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

View file

@ -372,7 +372,7 @@ function MenuTreeInline({
if (!node.id) return
if (
!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
@ -422,7 +422,7 @@ function MenuTreeInline({
<div className="px-4 py-3 text-sm text-gray-400">Loading</div>
) : enrichedNodes.length === 0 ? (
<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>
) : (
enrichedNodes.map((node) => (
@ -504,8 +504,7 @@ const WizardStep1 = ({
!values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
// 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.
!(Number(values.menuOrder) >= 1) &&
(translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'),
!(Number(values.menuOrder) >= 1) && translate('::ListForms.Wizard.Step1.MenuOrder'),
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
@ -521,8 +520,7 @@ const WizardStep1 = ({
asterisk={true}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.WizardNameHint') ||
'Used to generate ListForm Code and Menu Code'}
{translate('::ListForms.Wizard.Step1.WizardNameHint')}
</span>
}
>
@ -561,9 +559,7 @@ const WizardStep1 = ({
variant="solid"
icon={<FaPlus className="text-xs" />}
>
<span className="whitespace-nowrap">
{translate('::ListForms.Wizard.Add') || 'Ekle'}
</span>
<span className="whitespace-nowrap">{translate('::ListForms.Wizard.Add')}</span>
</Button>
{values.menuParentCode && (
<Button
@ -578,7 +574,7 @@ const WizardStep1 = ({
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"
>
{translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'}
{translate('::ListForms.Wizard.ClearSelection')}
</Button>
)}
</div>
@ -624,7 +620,7 @@ const WizardStep1 = ({
asterisk={true}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.MenuCodeHint') || 'Auto-derived, editable'}
{translate('::ListForms.Wizard.Step1.MenuCodeHint')}
</span>
}
>
@ -640,14 +636,13 @@ const WizardStep1 = ({
{/* Menu Order */}
<FormItem
label={translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'}
label={translate('::ListForms.Wizard.Step1.MenuOrder')}
asterisk={true}
invalid={!!(errors.menuOrder && touched.menuOrder)}
errorMessage={errors.menuOrder}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.Wizard.Step1.MenuOrderHint') ||
'Automatically assigned, editable'}
{translate('::ListForms.Wizard.Step1.MenuOrderHint')}
</span>
}
>
@ -690,9 +685,7 @@ const WizardStep1 = ({
type="text"
autoComplete="off"
name="languageTextMenuEn"
placeholder={
translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'
}
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameEnglish')}
component={Input}
/>
</FormItem>
@ -708,9 +701,7 @@ const WizardStep1 = ({
type="text"
autoComplete="off"
name="languageTextMenuTr"
placeholder={
translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'
}
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameTurkish')}
component={Input}
/>
</FormItem>
@ -756,7 +747,7 @@ const WizardStep1 = ({
<div className="flex-1 flex items-center gap-2">
{!step1CanGo && (
<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>
)}
</div>
@ -767,7 +758,7 @@ const WizardStep1 = ({
disabled={!step1CanGo}
onClick={onNext}
>
{translate('::Next') || 'Next'}
{translate('::Next')}
</Button>
</div>
</div>

View file

@ -188,10 +188,7 @@ const WizardStep11 = ({
const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false)
const steps = useMemo(
() => buildLogSteps(values, groups, translate),
[values, groups, translate],
)
const steps = useMemo(() => buildLogSteps(values, groups, translate), [values, groups, translate])
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@ -344,10 +341,8 @@ const WizardStep11 = ({
</div>
<Section
title={
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
title={translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout')}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm')}`}
>
<div className="mb-3 grid grid-cols-3 gap-2">
{[
@ -357,7 +352,7 @@ const WizardStep11 = ({
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,
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">
{g.items.length === 0 ? (
<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>
) : (
g.items.map((item) => {
@ -425,11 +420,11 @@ const WizardStep11 = ({
</div>
<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">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
{translate('::ListForms.Wizard.Step4.SelectedColumns')}
</span>
{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">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
{translate('::ListForms.Wizard.Step4.EditingForm')}
</span>
)}
{isKeyField && (
@ -455,7 +450,7 @@ const WizardStep11 = ({
{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="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 className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => {
@ -481,10 +476,7 @@ const WizardStep11 = ({
</Section>
{subForms.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
badge={subForms.length}
>
<Section title={translate('::ListForms.ListFormEdit.SubForms')} badge={subForms.length}>
<div className="flex flex-col gap-3">
{subForms.map((subForm, index) => (
<div
@ -517,10 +509,7 @@ const WizardStep11 = ({
)}
{widgets.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
badge={widgets.length}
>
<Section title={translate('::ListForms.ListFormEdit.TabWidgets')} badge={widgets.length}>
<div className="flex flex-col gap-3">
{widgets.map((widget, index) => (
<div
@ -545,7 +534,7 @@ const WizardStep11 = ({
{(hasWorkflowFields || workflowItems.length > 0) && (
<Section
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'}
title={translate('::ListForms.ListFormEdit.TabWorkflow')}
badge={workflowItems.length}
>
<div className="grid grid-cols-2 gap-3">
@ -594,7 +583,7 @@ const WizardStep11 = ({
)}
{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">
<Row
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.StatField'), value: totalFields },
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
label: translate('::ListForms.Wizard.Step4.EditingForm'),
value: editingFormFields.length,
},
{
@ -665,19 +654,19 @@ const WizardStep11 = ({
value: selectedColumns.size,
},
{
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
label: translate('::ListForms.ListFormEdit.SubForms'),
value: subForms.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
label: translate('::ListForms.ListFormEdit.TabWidgets'),
value: widgets.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
label: translate('::ListForms.ListFormEdit.TabWorkflow'),
value: workflowItems.length,
},
{
label: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo',
label: translate('::ListForms.ListFormEdit.TabTodo'),
value: values.todo ? 1 : 0,
},
].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">
<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" />
{translate('::ListForms.Wizard.Step4.DeployLog') || 'Deploy Log'}
{translate('::ListForms.Wizard.Step4.DeployLog')}
</span>
{isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
@ -771,7 +760,7 @@ const WizardStep11 = ({
disabled={isDeploying}
onClick={onBack}
>
{translate('::Back') || 'Back'}
{translate('::Back')}
</Button>
<div className="flex-1 flex items-center justify-end">
<Button

View file

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

View file

@ -387,7 +387,7 @@ function SortableItem({
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"
>
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
{translate('::ListForms.Wizard.Step3.GenerateFromTable')}
</Button>
</div>
<textarea
@ -423,7 +423,7 @@ function SortableItem({
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{pickerStep === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
? translate('::ListForms.Wizard.Step3.SelectTable')
: (pickerTable?.tableName ?? '')}
</span>
</div>
@ -445,15 +445,14 @@ function SortableItem({
autoFocus
value={tableSearch}
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"
/>
</div>
<div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') ||
'Tablo bulunamadı'}
{translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
</div>
) : (
dbObjects.tables
@ -506,14 +505,14 @@ function SortableItem({
<>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu
{translate('::ListForms.ListFormFieldEdit.KeyColumn')}
</label>
<select
value={pickerKeyCol}
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"
>
<option value="">-- Seçiniz --</option>
<option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
@ -523,14 +522,14 @@ function SortableItem({
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu
{translate('::ListForms.ListFormFieldEdit.NameColumn')}
</label>
<select
value={pickerNameCol}
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"
>
<option value="">-- Seçiniz --</option>
<option value="">{translate('::App.Select')}</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
@ -565,7 +564,7 @@ function SortableItem({
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"
>
Tamam
{translate('::OK')}
</Button>
</>
)}
@ -659,7 +658,7 @@ function SortableItem({
</div>
<label
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
type="checkbox"
@ -668,7 +667,7 @@ function SortableItem({
className="w-3 h-3 accent-indigo-500"
/>
<span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'}
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm')}
</span>
</label>
<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"
/>
<span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.Required') || 'Required'}
{translate('::ListForms.Wizard.Step3.Required')}
</span>
</label>
</div>
@ -741,7 +740,7 @@ function GroupCard({
{/* ColCount */}
<div className="flex items-center gap-1 shrink-0">
<span className="text-xs text-gray-400">
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
{translate('::ListForms.Wizard.Step3.Cols')}
</span>
{[1, 2, 3].map((n) => (
<Button
@ -769,12 +768,9 @@ function GroupCard({
shape="none"
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"
title={
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
'Tüm mevcut sütunları bu gruba ekle'
}
title={translate('::ListForms.Wizard.Step3.AddAllToGroupTitle')}
>
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
{translate('::ListForms.Wizard.Step3.AddAll')}
</Button>
)}
<Button
@ -784,7 +780,7 @@ function GroupCard({
shape="circle"
icon={<FaTrash className="text-xs" />}
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>
@ -800,7 +796,7 @@ function GroupCard({
>
{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">
{translate('::ListForms.Wizard.Step3.DragColumnsHere') || 'Sütunları buraya sürükleyin'}
{translate('::ListForms.Wizard.Step3.DragColumnsHere')}
</div>
)}
<SortableContext items={itemIds} strategy={rectSortingStrategy}>
@ -1085,10 +1081,9 @@ const WizardStep3 = ({
const hasEmptyGroup = groups.some((g) => g.items.length === 0)
const canProceed = !hasNoGroups && !hasEmptyGroup
const validationMsg = hasNoGroups
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.'
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup')
: hasEmptyGroup
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn') ||
'Her gruba en az bir sütun eklemelisiniz.'
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn')
: ''
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">
{availableColumns.length === 0 ? (
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full">
{translate('::ListForms.Wizard.Step3.AllColumnsAdded') ||
'Tüm sütunlar gruplara eklendi'}
{translate('::ListForms.Wizard.Step3.AllColumnsAdded')}
</div>
) : (
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
@ -1128,8 +1122,7 @@ const WizardStep3 = ({
<div className="flex-1 flex flex-col gap-3">
{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">
{translate('::ListForms.Wizard.Step3.NoGroupsYet') ||
'Henüz grup yok — aşağıdan grup ekleyin'}
{translate('::ListForms.Wizard.Step3.NoGroupsYet')}
</div>
)}
@ -1160,7 +1153,7 @@ const WizardStep3 = ({
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"
>
{translate('::ListForms.Wizard.Step3.AddGroup') || 'Grup Ekle'}
{translate('::ListForms.Wizard.Step3.AddGroup')}
</Button>
</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="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
{translate('::Back')}
</Button>
<Button
size="sm"
@ -1248,7 +1241,7 @@ const WizardStep3 = ({
icon={<FaCode />}
onClick={() => setIsHelperOpen(true)}
>
{translate('::Helper Codes') || 'Helper Codes'}
{translate('::Helper Codes')}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
{!canProceed && (
@ -1264,7 +1257,7 @@ const WizardStep3 = ({
disabled={!canProceed}
onClick={onNext}
>
{translate('::Next') || 'Next'}
{translate('::Next')}
</Button>
</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="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
{translate('::Back')}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
<Button
@ -229,7 +229,7 @@ function WizardStep4({
icon={<FaArrowRight />}
onClick={onNext}
>
{translate('::Next') || 'Next'}
{translate('::Next')}
</Button>
</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="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
{translate('::Back')}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
<Button
@ -153,7 +153,7 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
icon={<FaArrowRight />}
onClick={onNext}
>
{translate('::Next') || 'Next'}
{translate('::Next')}
</Button>
</div>
</div>

View file

@ -223,7 +223,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
id: startId,
nodeId: startId,
title: 'İş Akışı Başlat1',
title: translate('::ListForms.Workflow.Demo.Start'),
nextOnStart: approval1Id,
positionX: 34,
positionY: 28,
@ -232,7 +232,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approval1Id,
nodeId: approval1Id,
title: 'Onay1',
title: translate('::ListForms.Workflow.Demo.Approval1'),
nextOnApprove: approval2Id,
nextOnReject: informId,
positionX: 323,
@ -242,7 +242,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approval2Id,
nodeId: approval2Id,
title: 'Onay2',
title: translate('::ListForms.Workflow.Demo.Approval2'),
nextOnApprove: informId,
nextOnReject: informId,
positionX: 586,
@ -252,7 +252,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Inform', listFormCode)),
id: informId,
nodeId: informId,
title: 'Bilgilendirme1',
title: translate('::ListForms.Workflow.Demo.Inform'),
nextOnStart: endId,
positionX: 458,
positionY: 336,
@ -261,7 +261,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId,
nodeId: endId,
title: 'İş Akışı Bitir1',
title: translate('::ListForms.Workflow.Demo.End'),
positionX: 792,
positionY: 336,
} 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="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
{translate('::Back')}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
<Button
@ -480,7 +480,7 @@ function WizardStep6({
icon={<FaArrowRight />}
onClick={continueToTodo}
>
{translate('::Next') || 'Next'}
{translate('::Next')}
</Button>
</div>
</div>

View file

@ -7,6 +7,7 @@ import {
collectLinks,
getNodeOutcomes,
outcomeLabel,
OUTCOME_KEYS,
type WorkflowLink,
type WorkflowOutcome,
} from '@/utils/workflow/workflowHelpers'
@ -93,6 +94,8 @@ export function WorkflowCanvas({
)
const links = useMemo(() => collectLinks(arrowCriteria) as WorkflowLink[], [arrowCriteria])
const { translate } = useLocalization()
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== 'Delete') return
event.preventDefault()
@ -124,14 +127,14 @@ export function WorkflowCanvas({
>
{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">
{outcomeLabel(pendingLink.outcome)} çıkışı seçildi. Hedef akışı adımına tıklayın.
{translate('::ListForms.Workflow.PendingLinkHint', {
outcome: translate('::' + (outcomeLabel(pendingLink.outcome) ?? '')),
})}
</div>
)}
{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">
<span>
Üstteki butonlardan adım ekleyin, sonra çıkış etiketleriyle bağlantıları kurun.
</span>
<span>{translate('::ListForms.Workflow.EmptyCanvasHint')}</span>
</div>
)}
<div
@ -252,11 +255,7 @@ export function WorkflowCanvas({
aria-hidden="true"
>
{links.map((link) => (
<ArrowLabel
key={`${link.key}-label`}
link={link}
pendingLink={pendingLink}
/>
<ArrowLabel key={`${link.key}-label`} link={link} pendingLink={pendingLink} />
))}
</svg>
</div>
@ -387,7 +386,9 @@ function FlowNode({
onBeginLink(item.id, outcome.field)
}}
>
{outcome.label}
{outcome.label.startsWith('ListForms.')
? translate('::' + outcome.label)
: outcome.label}
</span>
))}
</div>
@ -503,13 +504,7 @@ function Arrow({
)
}
function ArrowLabel({
link,
pendingLink,
}: {
link: WorkflowLink
pendingLink: PendingLink
}) {
function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
if (!link.label) return null
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
@ -550,8 +545,8 @@ function ArrowLabel({
function linkTone(link: WorkflowLink) {
const field = link.sourcePort?.field || ''
const label = link.label || ''
if (field === 'nextOnReject' || label === 'Red') return 'reject'
if (field === 'nextOnApprove' || label === 'Onay') return 'approve'
if (field === 'nextOnReject' || label === OUTCOME_KEYS.nextOnReject) return 'reject'
if (field === 'nextOnApprove' || label === OUTCOME_KEYS.nextOnApprove) return 'approve'
if (field.startsWith('compareOutcomes:') || link.source.kind === 'Compare') return 'compare'
if (field === 'nextOnStart') return 'next'
return 'neutral'

View file

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

View file

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

View file

@ -222,7 +222,7 @@ const OrganizationUnits = () => {
}
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
{translate('::Save')}
</Notification>,
{
placement: 'bottom-end',
@ -234,7 +234,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -291,7 +291,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -327,7 +327,7 @@ const OrganizationUnits = () => {
await ouPut({ id, displayName: name })
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
{translate('::Save')}
</Notification>,
{
placement: 'bottom-end',
@ -338,7 +338,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -360,7 +360,7 @@ const OrganizationUnits = () => {
await ouPutMove(id, newParentId)
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
{translate('::Save')}
</Notification>,
{
placement: 'bottom-end',
@ -371,7 +371,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -428,8 +428,8 @@ const OrganizationUnits = () => {
<div className="file-actions">
<div className="flex gap-1 folderFileActions">
<Button
size='xs'
variant='plain'
size="xs"
variant="plain"
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"
icon={<FaUserPlus className="text-xs" />}
@ -437,8 +437,8 @@ const OrganizationUnits = () => {
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
></Button>
<Button
size='xs'
variant='plain'
size="xs"
variant="plain"
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"
icon={<FaEdit className="text-xs" />}
@ -446,8 +446,8 @@ const OrganizationUnits = () => {
title={translate('::Abp.Identity.OrganizationUnit.Rename')}
></Button>
<Button
size='xs'
variant='plain'
size="xs"
variant="plain"
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"
icon={<FaTrashAlt className="text-xs" />}
@ -562,9 +562,9 @@ const OrganizationUnits = () => {
<THead>
<Tr>
<Th></Th>
<Th>Adı Soyadı</Th>
<Th>E-Posta</Th>
<Th>Durum</Th>
<Th>{translate('::App.Platform.FullName')}</Th>
<Th>{translate('::Abp.Account.EmailAddress')}</Th>
<Th>{translate('::App.Platform.Status')}</Th>
</Tr>
</THead>
)}
@ -633,7 +633,7 @@ const OrganizationUnits = () => {
<THead>
<Tr>
<Th></Th>
<Th>Rol</Th>
<Th>{translate('::App.Platform.Role')}</Th>
</Tr>
</THead>
)}
@ -786,7 +786,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -857,7 +857,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -935,7 +935,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',
@ -984,7 +984,7 @@ const OrganizationUnits = () => {
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
{translate('::App.Platform.Error')}
</Notification>,
{
placement: 'bottom-end',

View file

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

View file

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

View file

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

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