Eksik LanguageText keyler tanımlandı
This commit is contained in:
parent
b5cba69e28
commit
f372c17249
166 changed files with 17380 additions and 8509 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useState, useRef, useCallback } from "react";
|
import React, { useState, useRef, useCallback } from 'react'
|
||||||
|
|
||||||
interface SplitterProps {
|
interface SplitterProps {
|
||||||
direction: "horizontal" | "vertical";
|
direction: 'horizontal' | 'vertical'
|
||||||
initialSize: number;
|
initialSize: number
|
||||||
minSize?: number;
|
minSize?: number
|
||||||
maxSize?: number;
|
maxSize?: number
|
||||||
children: [React.ReactNode, React.ReactNode];
|
children: [React.ReactNode, React.ReactNode]
|
||||||
className?: string;
|
className?: string
|
||||||
reverse?: boolean; // İkinci panel için boyut kontrolü
|
reverse?: boolean // İkinci panel için boyut kontrolü
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Splitter: React.FC<SplitterProps> = ({
|
export const Splitter: React.FC<SplitterProps> = ({
|
||||||
|
|
@ -16,66 +16,66 @@ export const Splitter: React.FC<SplitterProps> = ({
|
||||||
minSize = 200,
|
minSize = 200,
|
||||||
maxSize = 800,
|
maxSize = 800,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = '',
|
||||||
reverse = false,
|
reverse = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [size, setSize] = useState(initialSize);
|
const [size, setSize] = useState(initialSize)
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const splitterRef = useRef<HTMLDivElement>(null);
|
const splitterRef = useRef<HTMLDivElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setIsDragging(true);
|
setIsDragging(true)
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!splitterRef.current || !containerRef.current) return;
|
if (!splitterRef.current || !containerRef.current) return
|
||||||
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect()
|
||||||
let newSize: number;
|
let newSize: number
|
||||||
|
|
||||||
if (direction === "horizontal") {
|
if (direction === 'horizontal') {
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
// İkinci panel için boyut kontrolü - sağdan ölçüm
|
// İkinci panel için boyut kontrolü - sağdan ölçüm
|
||||||
newSize = rect.right - e.clientX;
|
newSize = rect.right - e.clientX
|
||||||
} else {
|
} else {
|
||||||
// İlk panel için boyut kontrolü - soldan ölçüm
|
// İlk panel için boyut kontrolü - soldan ölçüm
|
||||||
newSize = e.clientX - rect.left;
|
newSize = e.clientX - rect.left
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
// İkinci panel için boyut kontrolü - alttan ölçüm
|
// İkinci panel için boyut kontrolü - alttan ölçüm
|
||||||
newSize = rect.bottom - e.clientY;
|
newSize = rect.bottom - e.clientY
|
||||||
} else {
|
} else {
|
||||||
// İlk panel için boyut kontrolü - üstten ölçüm
|
// İlk panel için boyut kontrolü - üstten ölçüm
|
||||||
newSize = e.clientY - rect.top;
|
newSize = e.clientY - rect.top
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSize = Math.max(minSize, Math.min(maxSize, newSize));
|
newSize = Math.max(minSize, Math.min(maxSize, newSize))
|
||||||
setSize(newSize);
|
setSize(newSize)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
setIsDragging(false);
|
setIsDragging(false)
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
};
|
}
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
},
|
},
|
||||||
[direction, minSize, maxSize, reverse]
|
[direction, minSize, maxSize, reverse],
|
||||||
);
|
)
|
||||||
|
|
||||||
const isHorizontal = direction === "horizontal";
|
const isHorizontal = direction === 'horizontal'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`flex flex-1 min-h-0 ${
|
className={`flex flex-1 min-h-0 ${
|
||||||
isHorizontal ? "flex-row" : "flex-col"
|
isHorizontal ? 'flex-row' : 'flex-col'
|
||||||
} h-full w-full ${className}`}
|
} h-full w-full ${className}`}
|
||||||
>
|
>
|
||||||
{reverse ? (
|
{reverse ? (
|
||||||
|
|
@ -85,20 +85,18 @@ export const Splitter: React.FC<SplitterProps> = ({
|
||||||
<div
|
<div
|
||||||
ref={splitterRef}
|
ref={splitterRef}
|
||||||
className={`
|
className={`
|
||||||
${
|
${isHorizontal ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
|
||||||
isHorizontal ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"
|
|
||||||
}
|
|
||||||
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
|
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
|
||||||
${isDragging ? "bg-blue-500 dark:bg-blue-600" : ""}
|
${isDragging ? 'bg-blue-500 dark:bg-blue-600' : ''}
|
||||||
`}
|
`}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
[isHorizontal ? "width" : "height"]: `${size}px`,
|
[isHorizontal ? 'width' : 'height']: `${size}px`,
|
||||||
[isHorizontal ? "minWidth" : "minHeight"]: `${size}px`,
|
[isHorizontal ? 'minWidth' : 'minHeight']: `${size}px`,
|
||||||
[isHorizontal ? "maxWidth" : "maxHeight"]: `${size}px`,
|
[isHorizontal ? 'maxWidth' : 'maxHeight']: `${size}px`,
|
||||||
}}
|
}}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|
@ -109,9 +107,9 @@ export const Splitter: React.FC<SplitterProps> = ({
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
[isHorizontal ? "width" : "height"]: `${size}px`,
|
[isHorizontal ? 'width' : 'height']: `${size}px`,
|
||||||
[isHorizontal ? "minWidth" : "minHeight"]: `${size}px`,
|
[isHorizontal ? 'minWidth' : 'minHeight']: `${size}px`,
|
||||||
[isHorizontal ? "maxWidth" : "maxHeight"]: `${size}px`,
|
[isHorizontal ? 'maxWidth' : 'maxHeight']: `${size}px`,
|
||||||
}}
|
}}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|
@ -121,11 +119,9 @@ export const Splitter: React.FC<SplitterProps> = ({
|
||||||
<div
|
<div
|
||||||
ref={splitterRef}
|
ref={splitterRef}
|
||||||
className={`
|
className={`
|
||||||
${
|
${isHorizontal ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
|
||||||
isHorizontal ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"
|
|
||||||
}
|
|
||||||
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
|
bg-gray-300 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex-shrink-0
|
||||||
${isDragging ? "bg-blue-500 dark:bg-blue-600" : ""}
|
${isDragging ? 'bg-blue-500 dark:bg-blue-600' : ''}
|
||||||
`}
|
`}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
|
|
@ -134,5 +130,5 @@ export const Splitter: React.FC<SplitterProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
|
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
|
||||||
import { Button } from '@/components/ui'
|
import { Button } from '@/components/ui'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
interface StyleModalProps {
|
interface StyleModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|
@ -12,30 +13,348 @@ interface StyleModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const STYLE_GROUPS = {
|
const STYLE_GROUPS = {
|
||||||
Display: ['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'contents', 'hidden'],
|
Display: [
|
||||||
Layout: ['container', 'box-border', 'box-content', 'overflow-auto', 'overflow-hidden', 'overflow-visible', 'overflow-scroll', 'overflow-x-auto', 'overflow-y-auto'],
|
'block',
|
||||||
Position: ['static', 'fixed', 'absolute', 'relative', 'sticky', 'inset-0', 'top-0', 'right-0', 'bottom-0', 'left-0', 'z-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'],
|
'inline-block',
|
||||||
Flexbox: ['flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse', 'flex-wrap', 'flex-nowrap', 'grow', 'grow-0', 'shrink', 'shrink-0', 'items-start', 'items-center', 'items-end', 'items-stretch', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'justify-evenly', 'self-auto', 'self-start', 'self-center', 'self-end', 'self-stretch'],
|
'inline',
|
||||||
Grid: ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5', 'grid-cols-6', 'grid-cols-12', 'col-auto', 'col-span-1', 'col-span-2', 'col-span-3', 'col-span-4', 'col-span-6', 'col-span-12', 'grid-rows-1', 'grid-rows-2', 'grid-rows-3', 'row-span-1', 'row-span-2', 'row-span-3'],
|
'flex',
|
||||||
Spacing: ['m-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-auto', 'mx-auto', 'my-auto', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mr-1', 'mr-2', 'mr-4', 'mb-1', 'mb-2', 'mb-4', 'ml-1', 'ml-2', 'ml-4', 'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'px-1', 'px-2', 'px-3', 'px-4', 'px-6', 'py-1', 'py-2', 'py-3', 'py-4', 'py-6', 'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'],
|
'inline-flex',
|
||||||
Sizing: ['w-0', 'w-auto', 'w-full', 'w-screen', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4', 'h-0', 'h-auto', 'h-full', 'h-screen', 'min-w-0', 'min-w-full', 'min-h-0', 'min-h-full', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-full', 'max-h-full', 'aspect-square', 'aspect-video'],
|
'grid',
|
||||||
Typography: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'font-thin', 'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'italic', 'not-italic', 'underline', 'line-through', 'no-underline', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize', 'truncate', 'whitespace-normal', 'whitespace-nowrap', 'break-words', 'leading-none', 'leading-tight', 'leading-normal', 'tracking-tight', 'tracking-normal', 'tracking-wide'],
|
'inline-grid',
|
||||||
Colors: ['text-transparent', 'text-white', 'text-black', 'text-slate-400', 'text-slate-500', 'text-slate-600', 'text-slate-700', 'text-slate-800', 'text-slate-900', 'text-sky-500', 'text-blue-600', 'text-red-500', 'text-amber-500', 'text-emerald-500', 'bg-transparent', 'bg-white', 'bg-black', 'bg-slate-50', 'bg-slate-100', 'bg-slate-200', 'bg-slate-700', 'bg-slate-800', 'bg-slate-900', 'bg-sky-500', 'bg-blue-600', 'bg-red-500', 'bg-amber-500', 'bg-emerald-500'],
|
'contents',
|
||||||
Border: ['border', 'border-0', 'border-2', 'border-t', 'border-r', 'border-b', 'border-l', 'border-transparent', 'border-slate-200', 'border-slate-300', 'border-slate-700', 'border-sky-500', 'rounded-none', 'rounded-sm', 'rounded', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full', 'divide-x', 'divide-y'],
|
'hidden',
|
||||||
Effects: ['shadow-none', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'blur-sm', 'blur', 'backdrop-blur-sm', 'backdrop-blur', 'transition', 'transition-all', 'duration-150', 'duration-300', 'ease-in', 'ease-out', 'ease-in-out'],
|
],
|
||||||
Interaction: ['cursor-auto', 'cursor-default', 'cursor-pointer', 'cursor-not-allowed', 'select-none', 'select-text', 'pointer-events-none', 'pointer-events-auto', 'resize', 'resize-none', 'outline-none', 'focus:outline-none', 'hover:opacity-80', 'hover:bg-slate-100', 'focus:ring-2', 'focus:ring-sky-500'],
|
Layout: [
|
||||||
Responsive: ['sm:block', 'sm:flex', 'sm:grid', 'sm:hidden', 'md:block', 'md:flex', 'md:grid', 'md:hidden', 'lg:block', 'lg:flex', 'lg:grid', 'lg:hidden', 'xl:block', 'xl:flex', 'xl:grid', 'dark:bg-slate-900', 'dark:bg-slate-950', 'dark:text-white', 'dark:text-slate-300', 'dark:border-slate-700'],
|
'container',
|
||||||
|
'box-border',
|
||||||
|
'box-content',
|
||||||
|
'overflow-auto',
|
||||||
|
'overflow-hidden',
|
||||||
|
'overflow-visible',
|
||||||
|
'overflow-scroll',
|
||||||
|
'overflow-x-auto',
|
||||||
|
'overflow-y-auto',
|
||||||
|
],
|
||||||
|
Position: [
|
||||||
|
'static',
|
||||||
|
'fixed',
|
||||||
|
'absolute',
|
||||||
|
'relative',
|
||||||
|
'sticky',
|
||||||
|
'inset-0',
|
||||||
|
'top-0',
|
||||||
|
'right-0',
|
||||||
|
'bottom-0',
|
||||||
|
'left-0',
|
||||||
|
'z-0',
|
||||||
|
'z-10',
|
||||||
|
'z-20',
|
||||||
|
'z-30',
|
||||||
|
'z-40',
|
||||||
|
'z-50',
|
||||||
|
],
|
||||||
|
Flexbox: [
|
||||||
|
'flex-row',
|
||||||
|
'flex-row-reverse',
|
||||||
|
'flex-col',
|
||||||
|
'flex-col-reverse',
|
||||||
|
'flex-wrap',
|
||||||
|
'flex-nowrap',
|
||||||
|
'grow',
|
||||||
|
'grow-0',
|
||||||
|
'shrink',
|
||||||
|
'shrink-0',
|
||||||
|
'items-start',
|
||||||
|
'items-center',
|
||||||
|
'items-end',
|
||||||
|
'items-stretch',
|
||||||
|
'justify-start',
|
||||||
|
'justify-center',
|
||||||
|
'justify-end',
|
||||||
|
'justify-between',
|
||||||
|
'justify-around',
|
||||||
|
'justify-evenly',
|
||||||
|
'self-auto',
|
||||||
|
'self-start',
|
||||||
|
'self-center',
|
||||||
|
'self-end',
|
||||||
|
'self-stretch',
|
||||||
|
],
|
||||||
|
Grid: [
|
||||||
|
'grid-cols-1',
|
||||||
|
'grid-cols-2',
|
||||||
|
'grid-cols-3',
|
||||||
|
'grid-cols-4',
|
||||||
|
'grid-cols-5',
|
||||||
|
'grid-cols-6',
|
||||||
|
'grid-cols-12',
|
||||||
|
'col-auto',
|
||||||
|
'col-span-1',
|
||||||
|
'col-span-2',
|
||||||
|
'col-span-3',
|
||||||
|
'col-span-4',
|
||||||
|
'col-span-6',
|
||||||
|
'col-span-12',
|
||||||
|
'grid-rows-1',
|
||||||
|
'grid-rows-2',
|
||||||
|
'grid-rows-3',
|
||||||
|
'row-span-1',
|
||||||
|
'row-span-2',
|
||||||
|
'row-span-3',
|
||||||
|
],
|
||||||
|
Spacing: [
|
||||||
|
'm-0',
|
||||||
|
'm-1',
|
||||||
|
'm-2',
|
||||||
|
'm-3',
|
||||||
|
'm-4',
|
||||||
|
'm-5',
|
||||||
|
'm-6',
|
||||||
|
'm-8',
|
||||||
|
'm-auto',
|
||||||
|
'mx-auto',
|
||||||
|
'my-auto',
|
||||||
|
'mt-1',
|
||||||
|
'mt-2',
|
||||||
|
'mt-3',
|
||||||
|
'mt-4',
|
||||||
|
'mr-1',
|
||||||
|
'mr-2',
|
||||||
|
'mr-4',
|
||||||
|
'mb-1',
|
||||||
|
'mb-2',
|
||||||
|
'mb-4',
|
||||||
|
'ml-1',
|
||||||
|
'ml-2',
|
||||||
|
'ml-4',
|
||||||
|
'p-0',
|
||||||
|
'p-1',
|
||||||
|
'p-2',
|
||||||
|
'p-3',
|
||||||
|
'p-4',
|
||||||
|
'p-5',
|
||||||
|
'p-6',
|
||||||
|
'p-8',
|
||||||
|
'px-1',
|
||||||
|
'px-2',
|
||||||
|
'px-3',
|
||||||
|
'px-4',
|
||||||
|
'px-6',
|
||||||
|
'py-1',
|
||||||
|
'py-2',
|
||||||
|
'py-3',
|
||||||
|
'py-4',
|
||||||
|
'py-6',
|
||||||
|
'gap-0',
|
||||||
|
'gap-1',
|
||||||
|
'gap-2',
|
||||||
|
'gap-3',
|
||||||
|
'gap-4',
|
||||||
|
'gap-6',
|
||||||
|
'gap-8',
|
||||||
|
],
|
||||||
|
Sizing: [
|
||||||
|
'w-0',
|
||||||
|
'w-auto',
|
||||||
|
'w-full',
|
||||||
|
'w-screen',
|
||||||
|
'w-1/2',
|
||||||
|
'w-1/3',
|
||||||
|
'w-2/3',
|
||||||
|
'w-1/4',
|
||||||
|
'w-3/4',
|
||||||
|
'h-0',
|
||||||
|
'h-auto',
|
||||||
|
'h-full',
|
||||||
|
'h-screen',
|
||||||
|
'min-w-0',
|
||||||
|
'min-w-full',
|
||||||
|
'min-h-0',
|
||||||
|
'min-h-full',
|
||||||
|
'max-w-xs',
|
||||||
|
'max-w-sm',
|
||||||
|
'max-w-md',
|
||||||
|
'max-w-lg',
|
||||||
|
'max-w-xl',
|
||||||
|
'max-w-2xl',
|
||||||
|
'max-w-full',
|
||||||
|
'max-h-full',
|
||||||
|
'aspect-square',
|
||||||
|
'aspect-video',
|
||||||
|
],
|
||||||
|
Typography: [
|
||||||
|
'text-xs',
|
||||||
|
'text-sm',
|
||||||
|
'text-base',
|
||||||
|
'text-lg',
|
||||||
|
'text-xl',
|
||||||
|
'text-2xl',
|
||||||
|
'text-3xl',
|
||||||
|
'font-thin',
|
||||||
|
'font-light',
|
||||||
|
'font-normal',
|
||||||
|
'font-medium',
|
||||||
|
'font-semibold',
|
||||||
|
'font-bold',
|
||||||
|
'italic',
|
||||||
|
'not-italic',
|
||||||
|
'underline',
|
||||||
|
'line-through',
|
||||||
|
'no-underline',
|
||||||
|
'text-left',
|
||||||
|
'text-center',
|
||||||
|
'text-right',
|
||||||
|
'uppercase',
|
||||||
|
'lowercase',
|
||||||
|
'capitalize',
|
||||||
|
'truncate',
|
||||||
|
'whitespace-normal',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
'break-words',
|
||||||
|
'leading-none',
|
||||||
|
'leading-tight',
|
||||||
|
'leading-normal',
|
||||||
|
'tracking-tight',
|
||||||
|
'tracking-normal',
|
||||||
|
'tracking-wide',
|
||||||
|
],
|
||||||
|
Colors: [
|
||||||
|
'text-transparent',
|
||||||
|
'text-white',
|
||||||
|
'text-black',
|
||||||
|
'text-slate-400',
|
||||||
|
'text-slate-500',
|
||||||
|
'text-slate-600',
|
||||||
|
'text-slate-700',
|
||||||
|
'text-slate-800',
|
||||||
|
'text-slate-900',
|
||||||
|
'text-sky-500',
|
||||||
|
'text-blue-600',
|
||||||
|
'text-red-500',
|
||||||
|
'text-amber-500',
|
||||||
|
'text-emerald-500',
|
||||||
|
'bg-transparent',
|
||||||
|
'bg-white',
|
||||||
|
'bg-black',
|
||||||
|
'bg-slate-50',
|
||||||
|
'bg-slate-100',
|
||||||
|
'bg-slate-200',
|
||||||
|
'bg-slate-700',
|
||||||
|
'bg-slate-800',
|
||||||
|
'bg-slate-900',
|
||||||
|
'bg-sky-500',
|
||||||
|
'bg-blue-600',
|
||||||
|
'bg-red-500',
|
||||||
|
'bg-amber-500',
|
||||||
|
'bg-emerald-500',
|
||||||
|
],
|
||||||
|
Border: [
|
||||||
|
'border',
|
||||||
|
'border-0',
|
||||||
|
'border-2',
|
||||||
|
'border-t',
|
||||||
|
'border-r',
|
||||||
|
'border-b',
|
||||||
|
'border-l',
|
||||||
|
'border-transparent',
|
||||||
|
'border-slate-200',
|
||||||
|
'border-slate-300',
|
||||||
|
'border-slate-700',
|
||||||
|
'border-sky-500',
|
||||||
|
'rounded-none',
|
||||||
|
'rounded-sm',
|
||||||
|
'rounded',
|
||||||
|
'rounded-md',
|
||||||
|
'rounded-lg',
|
||||||
|
'rounded-xl',
|
||||||
|
'rounded-2xl',
|
||||||
|
'rounded-full',
|
||||||
|
'divide-x',
|
||||||
|
'divide-y',
|
||||||
|
],
|
||||||
|
Effects: [
|
||||||
|
'shadow-none',
|
||||||
|
'shadow-sm',
|
||||||
|
'shadow',
|
||||||
|
'shadow-md',
|
||||||
|
'shadow-lg',
|
||||||
|
'shadow-xl',
|
||||||
|
'opacity-0',
|
||||||
|
'opacity-50',
|
||||||
|
'opacity-75',
|
||||||
|
'opacity-100',
|
||||||
|
'blur-sm',
|
||||||
|
'blur',
|
||||||
|
'backdrop-blur-sm',
|
||||||
|
'backdrop-blur',
|
||||||
|
'transition',
|
||||||
|
'transition-all',
|
||||||
|
'duration-150',
|
||||||
|
'duration-300',
|
||||||
|
'ease-in',
|
||||||
|
'ease-out',
|
||||||
|
'ease-in-out',
|
||||||
|
],
|
||||||
|
Interaction: [
|
||||||
|
'cursor-auto',
|
||||||
|
'cursor-default',
|
||||||
|
'cursor-pointer',
|
||||||
|
'cursor-not-allowed',
|
||||||
|
'select-none',
|
||||||
|
'select-text',
|
||||||
|
'pointer-events-none',
|
||||||
|
'pointer-events-auto',
|
||||||
|
'resize',
|
||||||
|
'resize-none',
|
||||||
|
'outline-none',
|
||||||
|
'focus:outline-none',
|
||||||
|
'hover:opacity-80',
|
||||||
|
'hover:bg-slate-100',
|
||||||
|
'focus:ring-2',
|
||||||
|
'focus:ring-sky-500',
|
||||||
|
],
|
||||||
|
Responsive: [
|
||||||
|
'sm:block',
|
||||||
|
'sm:flex',
|
||||||
|
'sm:grid',
|
||||||
|
'sm:hidden',
|
||||||
|
'md:block',
|
||||||
|
'md:flex',
|
||||||
|
'md:grid',
|
||||||
|
'md:hidden',
|
||||||
|
'lg:block',
|
||||||
|
'lg:flex',
|
||||||
|
'lg:grid',
|
||||||
|
'lg:hidden',
|
||||||
|
'xl:block',
|
||||||
|
'xl:flex',
|
||||||
|
'xl:grid',
|
||||||
|
'dark:bg-slate-900',
|
||||||
|
'dark:bg-slate-950',
|
||||||
|
'dark:text-white',
|
||||||
|
'dark:text-slate-300',
|
||||||
|
'dark:border-slate-700',
|
||||||
|
],
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const splitClasses = (value: string) => value.split(/\s+/).filter(Boolean)
|
const splitClasses = (value: string) => value.split(/\s+/).filter(Boolean)
|
||||||
|
|
||||||
const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose }: StyleModalProps) => {
|
const StyleModal = ({
|
||||||
|
isOpen,
|
||||||
|
mode,
|
||||||
|
propertyName,
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: StyleModalProps) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [customClass, setCustomClass] = useState('')
|
const [customClass, setCustomClass] = useState('')
|
||||||
const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | 'Tümü'>('Tümü')
|
const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | '__all__'>('__all__')
|
||||||
const selected = useMemo(() => new Set(splitClasses(value)), [value])
|
const selected = useMemo(() => new Set(splitClasses(value)), [value])
|
||||||
const classes = useMemo(() => {
|
const classes = useMemo(() => {
|
||||||
const source = category === 'Tümü' ? Object.values(STYLE_GROUPS).flat() : STYLE_GROUPS[category]
|
const source =
|
||||||
|
category === '__all__' ? Object.values(STYLE_GROUPS).flat() : STYLE_GROUPS[category]
|
||||||
const query = search.trim().toLowerCase()
|
const query = search.trim().toLowerCase()
|
||||||
return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query))
|
return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query))
|
||||||
}, [category, search])
|
}, [category, search])
|
||||||
|
|
@ -56,41 +375,112 @@ const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4" onMouseDown={onClose}>
|
<div
|
||||||
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900" onMouseDown={(event) => event.stopPropagation()}>
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onMouseDown={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900"
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700">
|
<div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold text-slate-900 dark:text-white">StyleModal · {propertyName}</h2>
|
<h2 className="font-semibold text-slate-900 dark:text-white">
|
||||||
<p className="mt-1 text-xs text-slate-500">{mode === 'css' ? 'CSS kodunu doğrudan düzenleyin.' : 'Hazır sınıfları seçin veya CSS/Tailwind class adlarını elle düzenleyin.'}</p>
|
StyleModal · {propertyName}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-xs text-slate-500">
|
||||||
|
{translate(
|
||||||
|
mode === 'css' ? '::App.StyleModal.CssModeHint' : '::App.StyleModal.ClassModeHint',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
|
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
|
||||||
</div>
|
</div>
|
||||||
{mode === 'class' && <div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
|
{mode === 'class' && (
|
||||||
<label className="relative">
|
<div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
|
||||||
<FaSearch className="absolute left-3 top-3 text-slate-400" />
|
<label className="relative">
|
||||||
<input className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Hazır sınıflarda ara…" value={search} onChange={(event) => setSearch(event.target.value)} />
|
<FaSearch className="absolute left-3 top-3 text-slate-400" />
|
||||||
</label>
|
<input
|
||||||
<select className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" value={category} onChange={(event) => setCategory(event.target.value as typeof category)}>
|
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
|
||||||
<option value="Tümü">Tüm kategoriler</option>
|
placeholder={translate('::App.StyleModal.SearchClasses')}
|
||||||
{Object.keys(STYLE_GROUPS).map((name) => <option key={name} value={name}>{name}</option>)}
|
value={search}
|
||||||
</select>
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
</div>}
|
/>
|
||||||
{mode === 'class' && <div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700">
|
</label>
|
||||||
<input className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Örn. hover:scale-105 w-[320px] [&>span]:font-bold" value={customClass} onChange={(event) => setCustomClass(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addCustomClass() }} />
|
<select
|
||||||
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>Ekle</Button>
|
className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white"
|
||||||
</div>}
|
value={category}
|
||||||
|
onChange={(event) => setCategory(event.target.value as typeof category)}
|
||||||
|
>
|
||||||
|
<option value="__all__">{translate('::App.StyleModal.AllCategories')}</option>
|
||||||
|
{Object.keys(STYLE_GROUPS).map((name) => (
|
||||||
|
<option key={name} value={name}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mode === 'class' && (
|
||||||
|
<div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700">
|
||||||
|
<input
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white"
|
||||||
|
placeholder={translate('::App.StyleModal.CustomClassPlaceholder')}
|
||||||
|
value={customClass}
|
||||||
|
onChange={(event) => setCustomClass(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') addCustomClass()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>
|
||||||
|
{translate('::ListForms.Wizard.Add')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
|
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
|
||||||
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">{propertyName} değerini elle düzenle</label>
|
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||||
<textarea className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`} placeholder={mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'} value={value} onChange={(event) => onChange(event.target.value)} />
|
{translate('::App.StyleModal.EditValueManually', { property: propertyName })}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`}
|
||||||
|
placeholder={
|
||||||
|
mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode === 'class' && <div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
|
{mode === 'class' && (
|
||||||
{classes.map((className) => (
|
<div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
<button key={className} className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`} type="button" onClick={() => toggleClass(className)}>{className}</button>
|
{classes.map((className) => (
|
||||||
))}
|
<button
|
||||||
</div>}
|
key={className}
|
||||||
|
className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleClass(className)}
|
||||||
|
>
|
||||||
|
{className}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
|
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
|
||||||
<span className="text-xs text-slate-500">{mode === 'css' ? 'Ham CSS düzenleme modu' : `${selected.size} seçili · ${classes.length} görüntüleniyor`}</span>
|
<span className="text-xs text-slate-500">
|
||||||
<div className="flex gap-2"><Button size="sm" variant="default" onClick={() => onChange('')}>Temizle</Button><Button size="sm" variant="solid" onClick={onClose}>Tamam</Button></div>
|
{mode === 'css'
|
||||||
|
? translate('::App.StyleModal.RawCssMode')
|
||||||
|
: translate('::App.StyleModal.SelectedShownCount', {
|
||||||
|
selected: selected.size,
|
||||||
|
shown: classes.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm" variant="default" onClick={() => onChange('')}>
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="solid" onClick={onClose}>
|
||||||
|
{translate('::OK')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,89 +1,80 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import {
|
import { FaCheckCircle, FaExclamationTriangle, FaClock, FaTimes } from 'react-icons/fa'
|
||||||
FaCheckCircle,
|
|
||||||
FaExclamationTriangle,
|
|
||||||
FaClock,
|
|
||||||
FaTimes,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
status: string;
|
status: string
|
||||||
size?: "sm" | "md";
|
size?: 'sm' | 'md'
|
||||||
showIcon?: boolean;
|
showIcon?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBadge: React.FC<StatusBadgeProps> = ({
|
const StatusBadge: React.FC<StatusBadgeProps> = ({ status, size = 'md', showIcon = true }) => {
|
||||||
status,
|
|
||||||
size = "md",
|
|
||||||
showIcon = true,
|
|
||||||
}) => {
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "px-1.5 py-0.5 text-xs",
|
sm: 'px-1.5 py-0.5 text-xs',
|
||||||
md: "px-2 py-0.5 text-xs",
|
md: 'px-2 py-0.5 text-xs',
|
||||||
};
|
}
|
||||||
|
|
||||||
const getStatusConfig = (status: string) => {
|
const getStatusConfig = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "active":
|
case 'active':
|
||||||
return {
|
return {
|
||||||
color: "bg-green-100 text-green-800",
|
color: 'bg-green-100 text-green-800',
|
||||||
icon: <FaCheckCircle size={14} />,
|
icon: <FaCheckCircle size={14} />,
|
||||||
text: "Aktif",
|
text: 'Aktif',
|
||||||
};
|
}
|
||||||
case "inactive":
|
case 'inactive':
|
||||||
return {
|
return {
|
||||||
color: "bg-red-100 text-red-800",
|
color: 'bg-red-100 text-red-800',
|
||||||
icon: <FaTimes size={14} />,
|
icon: <FaTimes size={14} />,
|
||||||
text: "Pasif",
|
text: 'Pasif',
|
||||||
};
|
}
|
||||||
case "pending":
|
case 'pending':
|
||||||
return {
|
return {
|
||||||
color: "bg-yellow-100 text-yellow-800",
|
color: 'bg-yellow-100 text-yellow-800',
|
||||||
icon: <FaClock size={14} />,
|
icon: <FaClock size={14} />,
|
||||||
text: "Beklemede",
|
text: 'Beklemede',
|
||||||
};
|
}
|
||||||
case "critical":
|
case 'critical':
|
||||||
return {
|
return {
|
||||||
color: "bg-red-100 text-red-800",
|
color: 'bg-red-100 text-red-800',
|
||||||
icon: <FaExclamationTriangle size={14} />,
|
icon: <FaExclamationTriangle size={14} />,
|
||||||
text: "Kritik",
|
text: 'Kritik',
|
||||||
};
|
}
|
||||||
case "low":
|
case 'low':
|
||||||
return {
|
return {
|
||||||
color: "bg-yellow-100 text-yellow-800",
|
color: 'bg-yellow-100 text-yellow-800',
|
||||||
icon: <FaExclamationTriangle size={14} />,
|
icon: <FaExclamationTriangle size={14} />,
|
||||||
text: "Düşük",
|
text: 'App.Platform.Priority.Low',
|
||||||
};
|
}
|
||||||
case "normal":
|
case 'normal':
|
||||||
return {
|
return {
|
||||||
color: "bg-green-100 text-green-800",
|
color: 'bg-green-100 text-green-800',
|
||||||
icon: <FaCheckCircle size={14} />,
|
icon: <FaCheckCircle size={14} />,
|
||||||
text: "Normal",
|
text: 'Normal',
|
||||||
};
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
color: "bg-gray-100 text-gray-800",
|
color: 'bg-gray-100 text-gray-800',
|
||||||
icon: <FaCheckCircle size={14} />,
|
icon: <FaCheckCircle size={14} />,
|
||||||
text: "Bilinmiyor",
|
text: 'Bilinmiyor',
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const config = getStatusConfig(status);
|
const config = getStatusConfig(status)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center rounded-full font-medium",
|
'inline-flex items-center rounded-full font-medium',
|
||||||
config.color,
|
config.color,
|
||||||
sizeClasses[size]
|
sizeClasses[size],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showIcon && <span className="mr-1">{config.icon}</span>}
|
{showIcon && <span className="mr-1">{config.icon}</span>}
|
||||||
{config.text}
|
{config.text}
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default StatusBadge;
|
export default StatusBadge
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import DynamicRenderer from './DynamicRenderer'
|
||||||
import { useComponents } from '@/contexts/ComponentContext'
|
import { useComponents } from '@/contexts/ComponentContext'
|
||||||
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
||||||
import { Loading } from '../shared'
|
import { Loading } from '../shared'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export interface ComponentPreviewProps {
|
export interface ComponentPreviewProps {
|
||||||
componentName?: string
|
componentName?: string
|
||||||
|
|
@ -10,6 +11,7 @@ export interface ComponentPreviewProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
|
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const { components, loading } = useComponents()
|
const { components, loading } = useComponents()
|
||||||
|
|
||||||
// Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor.
|
// Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor.
|
||||||
|
|
@ -19,7 +21,11 @@ const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, clas
|
||||||
}, [components, componentName])
|
}, [components, componentName])
|
||||||
|
|
||||||
if (!componentName) {
|
if (!componentName) {
|
||||||
return <div className="text-sm text-gray-500 dark:text-gray-400">Bileşen ismi yok.</div>
|
return (
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{translate('::App.DeveloperKit.ComponentPreview.NoComponentName')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || !Array.isArray(components)) {
|
if (loading || !Array.isArray(components)) {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import type { ErrorInfo } from "react";
|
import type { ErrorInfo } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
hasError: boolean;
|
hasError: boolean
|
||||||
error: Error | null;
|
error: Error | null
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component<Props, State> {
|
class ErrorBoundary extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props)
|
||||||
this.state = { hasError: false, error: null };
|
this.state = { hasError: false, error: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error): State {
|
static getDerivedStateFromError(error: Error): State {
|
||||||
return { hasError: true, error };
|
return { hasError: true, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
// Eğer component değişmişse, hata state'ini sıfırla
|
// Eğer component değişmişse, hata state'ini sıfırla
|
||||||
if (prevProps.children !== this.props.children && this.state.hasError) {
|
if (prevProps.children !== this.props.children && this.state.hasError) {
|
||||||
this.setState({ hasError: false, error: null });
|
this.setState({ hasError: false, error: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
console.error("Render hatası:", error, info);
|
console.error('Render hatası:', error, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -36,27 +36,27 @@ class ErrorBoundary extends React.Component<Props, State> {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: "red",
|
color: 'red',
|
||||||
background: "#fff0f0",
|
background: '#fff0f0',
|
||||||
border: "1px solid #f44336",
|
border: '1px solid #f44336',
|
||||||
padding: "1rem",
|
padding: '1rem',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
fontFamily: "monospace",
|
fontFamily: 'monospace',
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: 'pre-wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⚠️ Render Hatası:
|
⚠️ Render error:
|
||||||
<br />
|
<br />
|
||||||
<details open style={{ marginTop: "0.5rem" }}>
|
<details open style={{ marginTop: '0.5rem' }}>
|
||||||
<summary>Detayları Göster</summary>
|
<summary>Show details</summary>
|
||||||
{this.state.error?.message || String(this.state.error)}
|
{this.state.error?.message || String(this.state.error)}
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.props.children;
|
return this.props.children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorBoundary;
|
export default ErrorBoundary
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
const importService = useMemo(() => new ImportService(), [])
|
const importService = useMemo(() => new ImportService(), [])
|
||||||
const [generating, setGenerating] = useState(false)
|
const [generating, setGenerating] = useState(false)
|
||||||
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set())
|
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set())
|
||||||
const [sessionExecutes, setSessionExecutes] = useState<
|
const [sessionExecutes, setSessionExecutes] = useState<Record<string, ListFormImportLogDto[]>>({})
|
||||||
Record<string, ListFormImportLogDto[]>
|
|
||||||
>({})
|
|
||||||
const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set())
|
const [loadingExecutes, setLoadingExecutes] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -414,7 +412,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
{editableColumns.map((column: any) => (
|
{editableColumns.map((column: any) => (
|
||||||
<tr key={column.fieldName} className="hover:bg-gray-50 dark:hover:bg-gray-800/70">
|
<tr
|
||||||
|
key={column.fieldName}
|
||||||
|
className="hover:bg-gray-50 dark:hover:bg-gray-800/70"
|
||||||
|
>
|
||||||
<td className="px-2 py-2 font-medium text-gray-800 dark:text-gray-100">
|
<td className="px-2 py-2 font-medium text-gray-800 dark:text-gray-100">
|
||||||
{column.fieldName}
|
{column.fieldName}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -740,8 +741,14 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{execute.errorRows} hata detayı
|
{translate('::App.Listforms.ImportManager.ErrorDetails', {
|
||||||
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
|
count: execute.errorRows,
|
||||||
|
})}{' '}
|
||||||
|
{translate(
|
||||||
|
expandedErrors.has(execute.id)
|
||||||
|
? '::App.Platform.Hide'
|
||||||
|
: '::App.Platform.Show',
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -752,10 +759,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
<thead className="bg-orange-100 dark:bg-gray-800 sticky top-0">
|
<thead className="bg-orange-100 dark:bg-gray-800 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
|
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
|
||||||
Satır
|
{translate('::App.Platform.Row')}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
|
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
|
||||||
Hata Mesajı
|
{translate('::App.Platform.ErrorMessage')}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -777,7 +784,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
|
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
|
||||||
Hata detayı mevcut değil.
|
{translate(
|
||||||
|
'::App.Listforms.ImportManager.NoErrorDetail',
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,7 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
|
||||||
{getStatusMessage()}
|
{getStatusMessage()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
{translate('::App.Listforms.Status.Processing')}{' '}
|
{translate('::App.Listforms.Status.Processing')} {session.blobName}
|
||||||
{session.blobName}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,7 @@ const layouts = {
|
||||||
const AuthLayout = lazy(() => import('./AuthLayout'))
|
const AuthLayout = lazy(() => import('./AuthLayout'))
|
||||||
const PublicLayout = lazy(() => import('./PublicLayout'))
|
const PublicLayout = lazy(() => import('./PublicLayout'))
|
||||||
|
|
||||||
class LayoutChunkErrorBoundary extends Component<
|
class LayoutChunkErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
||||||
{ children: ReactNode },
|
|
||||||
{ failed: boolean }
|
|
||||||
> {
|
|
||||||
state = { failed: false }
|
state = { failed: false }
|
||||||
|
|
||||||
static getDerivedStateFromError() {
|
static getDerivedStateFromError() {
|
||||||
|
|
@ -57,16 +54,21 @@ class LayoutChunkErrorBoundary extends Component<
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center p-6">
|
<div className="flex min-h-screen items-center justify-center p-6">
|
||||||
<div className="max-w-md text-center">
|
<div className="max-w-md text-center">
|
||||||
<h1 className="mb-2 text-lg font-semibold">Uygulama kabuğu yüklenemedi</h1>
|
{/*
|
||||||
|
Bu sınır, uygulama konfigürasyonu (ve dolayısıyla lokalizasyon metinleri)
|
||||||
|
yüklenemeden devreye girer; `translate` burada anahtarın kendisini döndürürdü.
|
||||||
|
Bu yüzden açılış öncesi ekranların tamamı gibi sabit İngilizce tutulur.
|
||||||
|
*/}
|
||||||
|
<h1 className="mb-2 text-lg font-semibold">Application shell could not be loaded</h1>
|
||||||
<p className="mb-4 text-sm text-gray-500">
|
<p className="mb-4 text-sm text-gray-500">
|
||||||
Bağlantınızı kontrol edip uygulamayı yeniden yükleyin.
|
Check your connection and reload the application.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white"
|
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
Yeniden yükle
|
Reload
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ const PublicLayout = () => {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
|
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
|
||||||
aria-pressed={!isDarkMode}
|
aria-pressed={!isDarkMode}
|
||||||
title="Light mode"
|
title={translate('::App.Theme.LightMode')}
|
||||||
icon={<LuSun size={15} strokeWidth={1.8} />}
|
icon={<LuSun size={15} strokeWidth={1.8} />}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
@ -151,7 +151,7 @@ const PublicLayout = () => {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
|
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
|
||||||
aria-pressed={isDarkMode}
|
aria-pressed={isDarkMode}
|
||||||
title="Dark mode"
|
title={translate('::App.Theme.DarkMode')}
|
||||||
icon={<LuMoon size={15} strokeWidth={1.8} />}
|
icon={<LuMoon size={15} strokeWidth={1.8} />}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
@ -174,422 +174,289 @@ const PublicLayout = () => {
|
||||||
<DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}>
|
<DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}>
|
||||||
<ScrollContext.Provider value={scrolled}>
|
<ScrollContext.Provider value={scrolled}>
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header
|
<header
|
||||||
className={`fixed w-full z-50 transition-all duration-500 ${
|
className={`fixed w-full z-50 transition-all duration-500 ${
|
||||||
scrolled
|
scrolled
|
||||||
? 'bg-gray-950 shadow-lg shadow-black/30 py-2 border-b border-white/10'
|
? 'bg-gray-950 shadow-lg shadow-black/30 py-2 border-b border-white/10'
|
||||||
: 'bg-gray-950/80 backdrop-blur-sm py-4 border-b border-white/5'
|
: 'bg-gray-950/80 backdrop-blur-sm py-4 border-b border-white/5'
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="container mx-auto px-6 relative grid grid-cols-[auto_1fr_auto] items-center lg:grid-cols-[1fr_auto] xl:grid-cols-[auto_1fr_auto]">
|
|
||||||
<Logo
|
|
||||||
mode={isDarkMode ? 'dark' : 'light'}
|
|
||||||
className="relative z-10 max-w-[190px] overflow-hidden lg:hidden xl:block xl:max-w-none"
|
|
||||||
imgClass="h-10 w-auto object-contain xl:h-auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Desktop / tablet navigation */}
|
|
||||||
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
|
|
||||||
{navLinks
|
|
||||||
.filter((l) => !isLoginLink(l.resourceKey))
|
|
||||||
.map((link) => {
|
|
||||||
const active = isActiveLink(link.path)
|
|
||||||
const baseClass =
|
|
||||||
'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group'
|
|
||||||
const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white'
|
|
||||||
return link.path ? (
|
|
||||||
<Link key={link.path} to={link.path} className={`${baseClass} ${activeClass}`}>
|
|
||||||
{link.icon && (
|
|
||||||
<link.icon
|
|
||||||
size={22}
|
|
||||||
strokeWidth={1.75}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? 'text-blue-400'
|
|
||||||
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{link.name}
|
|
||||||
<span
|
|
||||||
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
|
|
||||||
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
key={link.name}
|
|
||||||
onClick={link.action}
|
|
||||||
variant="plain"
|
|
||||||
size="sm"
|
|
||||||
className={`${baseClass} ${activeClass}`}
|
|
||||||
>
|
|
||||||
{link.icon && (
|
|
||||||
<link.icon
|
|
||||||
size={22}
|
|
||||||
strokeWidth={1.75}
|
|
||||||
className="text-gray-400 group-hover:text-blue-400 transition-colors"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{link.name}
|
|
||||||
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
|
|
||||||
{demoButton('rounded-lg')}
|
|
||||||
<div className="h-5 w-px bg-white/20" />
|
|
||||||
<div className="flex h-9 w-9 items-center justify-center">
|
|
||||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="h-5 w-px bg-white/20" />
|
|
||||||
{themeToggle}
|
|
||||||
<div className="h-5 w-px bg-white/20" />
|
|
||||||
{navLinks
|
|
||||||
.filter((l) => isLoginLink(l.resourceKey))
|
|
||||||
.map((link) =>
|
|
||||||
link.path ? (
|
|
||||||
<Button
|
|
||||||
key={link.path}
|
|
||||||
type="button"
|
|
||||||
variant="solid"
|
|
||||||
shape="round"
|
|
||||||
onClick={() => navigate(link.path!)}
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</Button>
|
|
||||||
) : null,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
|
||||||
<Button
|
|
||||||
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
|
|
||||||
onClick={toggleMenu}
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
icon={
|
|
||||||
isOpen ? (
|
|
||||||
<LuX size={20} strokeWidth={2} />
|
|
||||||
) : (
|
|
||||||
<LuMenu size={20} strokeWidth={2} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="plain"
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Navigation */}
|
|
||||||
<div
|
|
||||||
className={`lg:hidden transition-all duration-300 ease-in-out ${
|
|
||||||
isOpen
|
|
||||||
? 'max-h-screen overflow-visible opacity-100'
|
|
||||||
: 'max-h-0 overflow-hidden opacity-0'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md">
|
<div className="container mx-auto px-6 relative grid grid-cols-[auto_1fr_auto] items-center lg:grid-cols-[1fr_auto] xl:grid-cols-[auto_1fr_auto]">
|
||||||
<div className="container mx-auto px-6 py-3">
|
<Logo
|
||||||
<nav className="flex flex-col gap-1">
|
mode={isDarkMode ? 'dark' : 'light'}
|
||||||
<div className="lg:hidden flex flex-col gap-1">
|
className="relative z-10 max-w-[190px] overflow-hidden lg:hidden xl:block xl:max-w-none"
|
||||||
{navLinks.map((link) => {
|
imgClass="h-10 w-auto object-contain xl:h-auto"
|
||||||
const active = isActiveLink(link.path)
|
/>
|
||||||
const isLogin = isLoginLink(link.resourceKey)
|
|
||||||
if (isLogin) {
|
{/* Desktop / tablet navigation */}
|
||||||
|
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
|
||||||
|
{navLinks
|
||||||
|
.filter((l) => !isLoginLink(l.resourceKey))
|
||||||
|
.map((link) => {
|
||||||
|
const active = isActiveLink(link.path)
|
||||||
|
const baseClass =
|
||||||
|
'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group'
|
||||||
|
const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white'
|
||||||
|
return link.path ? (
|
||||||
|
<Link
|
||||||
|
key={link.path}
|
||||||
|
to={link.path}
|
||||||
|
className={`${baseClass} ${activeClass}`}
|
||||||
|
>
|
||||||
|
{link.icon && (
|
||||||
|
<link.icon
|
||||||
|
size={22}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className={
|
||||||
|
active
|
||||||
|
? 'text-blue-400'
|
||||||
|
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{link.name}
|
||||||
|
<span
|
||||||
|
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
|
||||||
|
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
key={link.name}
|
||||||
|
onClick={link.action}
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={`${baseClass} ${activeClass}`}
|
||||||
|
>
|
||||||
|
{link.icon && (
|
||||||
|
<link.icon
|
||||||
|
size={22}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="text-gray-400 group-hover:text-blue-400 transition-colors"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{link.name}
|
||||||
|
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
|
||||||
|
{demoButton('rounded-lg')}
|
||||||
|
<div className="h-5 w-px bg-white/20" />
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center">
|
||||||
|
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="h-5 w-px bg-white/20" />
|
||||||
|
{themeToggle}
|
||||||
|
<div className="h-5 w-px bg-white/20" />
|
||||||
|
{navLinks
|
||||||
|
.filter((l) => isLoginLink(l.resourceKey))
|
||||||
|
.map((link) =>
|
||||||
|
link.path ? (
|
||||||
|
<Button
|
||||||
|
key={link.path}
|
||||||
|
type="button"
|
||||||
|
variant="solid"
|
||||||
|
shape="round"
|
||||||
|
onClick={() => navigate(link.path!)}
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</Button>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<Button
|
||||||
|
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
|
||||||
|
onClick={toggleMenu}
|
||||||
|
aria-label={translate('::App.Platform.ToggleMenu')}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
icon={
|
||||||
|
isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />
|
||||||
|
}
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
<div
|
||||||
|
className={`lg:hidden transition-all duration-300 ease-in-out ${
|
||||||
|
isOpen
|
||||||
|
? 'max-h-screen overflow-visible opacity-100'
|
||||||
|
: 'max-h-0 overflow-hidden opacity-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md">
|
||||||
|
<div className="container mx-auto px-6 py-3">
|
||||||
|
<nav className="flex flex-col gap-1">
|
||||||
|
<div className="lg:hidden flex flex-col gap-1">
|
||||||
|
{navLinks.map((link) => {
|
||||||
|
const active = isActiveLink(link.path)
|
||||||
|
const isLogin = isLoginLink(link.resourceKey)
|
||||||
|
if (isLogin) {
|
||||||
|
return link.path ? (
|
||||||
|
<Link
|
||||||
|
key={link.path}
|
||||||
|
to={link.path}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
return link.path ? (
|
return link.path ? (
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={link.path}
|
||||||
to={link.path}
|
to={link.path}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
|
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
active
|
||||||
|
? 'bg-white/10 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-white/5 hover:text-white'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
{link.icon && (
|
||||||
|
<link.icon
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className={active ? 'text-blue-400' : 'text-gray-400'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : (
|
||||||
}
|
<Button
|
||||||
return link.path ? (
|
key={link.name}
|
||||||
<Link
|
onClick={() => {
|
||||||
key={link.path}
|
link.action?.()
|
||||||
to={link.path}
|
toggleMenu()
|
||||||
onClick={toggleMenu}
|
}}
|
||||||
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${
|
variant="plain"
|
||||||
active
|
size="sm"
|
||||||
? 'bg-white/10 text-white'
|
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
|
||||||
: 'text-gray-300 hover:bg-white/5 hover:text-white'
|
>
|
||||||
}`}
|
{link.icon && (
|
||||||
>
|
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
||||||
{link.icon && (
|
)}
|
||||||
<link.icon
|
{link.name}
|
||||||
size={16}
|
</Button>
|
||||||
strokeWidth={1.75}
|
)
|
||||||
className={active ? 'text-blue-400' : 'text-gray-400'}
|
})}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{link.name}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
key={link.name}
|
|
||||||
onClick={() => {
|
|
||||||
link.action?.()
|
|
||||||
toggleMenu()
|
|
||||||
}}
|
|
||||||
variant="plain"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
|
|
||||||
>
|
|
||||||
{link.icon && (
|
|
||||||
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
|
||||||
)}
|
|
||||||
{link.name}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{demoButton('rounded-lg', toggleMenu)}
|
|
||||||
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
|
|
||||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
|
|
||||||
{navLinks
|
|
||||||
.filter((l) => isLoginLink(l.resourceKey))
|
|
||||||
.map((link) =>
|
|
||||||
link.path ? (
|
|
||||||
<Link
|
|
||||||
key={link.path}
|
|
||||||
to={link.path}
|
|
||||||
onClick={toggleMenu}
|
|
||||||
className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</Link>
|
|
||||||
) : null,
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
|
||||||
</nav>
|
<div className="flex items-center justify-between gap-3">
|
||||||
</div>
|
<div className="flex items-center gap-1">
|
||||||
</div>
|
{demoButton('rounded-lg', toggleMenu)}
|
||||||
</div>
|
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
|
||||||
</header>
|
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||||
|
</div>
|
||||||
<main className="flex-grow">
|
</div>
|
||||||
<View />
|
<div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
|
||||||
</main>
|
{navLinks
|
||||||
|
.filter((l) => isLoginLink(l.resourceKey))
|
||||||
<footer className="bg-gray-900 text-white pt-16 pb-8">
|
.map((link) =>
|
||||||
<div className="container mx-auto px-4">
|
link.path ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
<Link
|
||||||
<div>
|
key={link.path}
|
||||||
<Logo mode={isDarkMode ? 'dark' : 'light'} />
|
to={link.path}
|
||||||
<p className="mt-4 text-gray-400 text-sm">
|
onClick={toggleMenu}
|
||||||
{translate('::Public.footer.companyInfo')}
|
className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
|
||||||
</p>
|
>
|
||||||
<div className="flex space-x-4 mt-6">
|
{link.name}
|
||||||
<a
|
</Link>
|
||||||
href="https://facebook.com/sozsoft"
|
) : null,
|
||||||
target="_blank"
|
)}
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
</div>
|
||||||
>
|
</nav>
|
||||||
<FaFacebook size={20} />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/sozsoft"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<FaTwitter size={20} />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://linkedin.com/sozsoft"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<FaLinkedin size={20} />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://instagram.com/sozsoft"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<FaInstagram size={20} />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold mb-4 text-white">
|
|
||||||
{translate('::Public.footer.quickLinksTitle')}
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.home}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Home')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.products}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Orders')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Services')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.about}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.About')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.blog}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.BlogManagement')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.contact}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Contact')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Services */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold mb-4 text-white">
|
|
||||||
{translate('::Public.services.title')}
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.software.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.web.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.mobile.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.database.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.integration.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
to={ROUTES_ENUM.public.services}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Public.services.consulting.title')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Info */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold mb-4 text-white">{translate('::App.Contact')}</h3>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
<li className="flex items-start space-x-3">
|
|
||||||
<LuMapPin
|
|
||||||
size={18}
|
|
||||||
strokeWidth={1.75}
|
|
||||||
className="text-gray-400 mt-1 flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span className="text-gray-400">{translate('::Public.footer.address')}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center space-x-3">
|
|
||||||
<LuPhone size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" />
|
|
||||||
<a
|
|
||||||
href="tel:+905447697638"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
+90 (544) 769 7 638
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center space-x-3">
|
|
||||||
<LuMail size={18} strokeWidth={1.75} className="text-gray-400 flex-shrink-0" />
|
|
||||||
<a
|
|
||||||
href="mailto:destek@sozsoft.com"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
destek@sozsoft.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="border-t border-gray-800 mt-12 pt-8">
|
<main className="flex-grow">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
<View />
|
||||||
<p className="text-gray-400 text-sm">
|
</main>
|
||||||
© {currentYear} Sözsoft Platform. {translate('::Public.footer.copyright')}
|
|
||||||
</p>
|
<footer className="bg-gray-900 text-white pt-16 pb-8">
|
||||||
<div className="mt-4 md:mt-0">
|
<div className="container mx-auto px-4">
|
||||||
<ul className="flex space-x-6 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
<div>
|
||||||
|
<Logo mode={isDarkMode ? 'dark' : 'light'} />
|
||||||
|
<p className="mt-4 text-gray-400 text-sm">
|
||||||
|
{translate('::Public.footer.companyInfo')}
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-4 mt-6">
|
||||||
|
<a
|
||||||
|
href="https://facebook.com/sozsoft"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<FaFacebook size={20} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/sozsoft"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<FaTwitter size={20} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://linkedin.com/sozsoft"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<FaLinkedin size={20} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://instagram.com/sozsoft"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<FaInstagram size={20} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4 text-white">
|
||||||
|
{translate('::Public.footer.quickLinksTitle')}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
to={ROUTES_ENUM.public.about}
|
to={ROUTES_ENUM.public.home}
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::Public.footer.privacyPolicy')}
|
{translate('::App.Home')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.products}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::App.Orders')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::App.Services')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -597,17 +464,160 @@ const PublicLayout = () => {
|
||||||
to={ROUTES_ENUM.public.about}
|
to={ROUTES_ENUM.public.about}
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::Public.footer.termsOfUse')}
|
{translate('::App.About')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.blog}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::App.BlogManagement')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.contact}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::App.Contact')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4 text-white">
|
||||||
|
{translate('::Public.services.title')}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.software.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.web.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.mobile.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.database.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.integration.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.services}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.services.consulting.title')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4 text-white">
|
||||||
|
{translate('::App.Contact')}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<LuMapPin
|
||||||
|
size={18}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="text-gray-400 mt-1 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-gray-400">{translate('::Public.footer.address')}</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-3">
|
||||||
|
<LuPhone
|
||||||
|
size={18}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="text-gray-400 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="tel:+905447697638"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
+90 (544) 769 7 638
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-3">
|
||||||
|
<LuMail
|
||||||
|
size={18}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="text-gray-400 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="mailto:destek@sozsoft.com"
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
destek@sozsoft.com
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-800 mt-12 pt-8">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
© {currentYear} Sözsoft Platform. {translate('::Public.footer.copyright')}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 md:mt-0">
|
||||||
|
<ul className="flex space-x-6 text-sm">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.about}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.footer.privacyPolicy')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={ROUTES_ENUM.public.about}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{translate('::Public.footer.termsOfUse')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
|
||||||
|
|
||||||
{/* Demo Modal */}
|
{/* Demo Modal */}
|
||||||
<Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
|
<Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollContext.Provider>
|
</ScrollContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const Basket: React.FC<BasketProps> = ({
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const handleClearBasket = () => {
|
const handleClearBasket = () => {
|
||||||
if (window.confirm('Sepetteki tüm ürünleri silmek istediğinizden emin misiniz?')) {
|
if (window.confirm(translate('::Public.basket.clearConfirm'))) {
|
||||||
clearBasket()
|
clearBasket()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
|
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
|
||||||
size="xs"
|
size="xs"
|
||||||
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
|
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
|
||||||
title={
|
title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
|
||||||
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{translate('::Public.products.billingcycle.monthly')}
|
{translate('::Public.products.billingcycle.monthly')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -65,9 +63,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
|
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
|
||||||
size="xs"
|
size="xs"
|
||||||
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
|
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
|
||||||
title={
|
title={hasBasketItems ? translate('::Public.basket.billingCycleLocked') : undefined}
|
||||||
hasBasketItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{translate('::Public.products.billingcycle.yearly')}
|
{translate('::Public.products.billingcycle.yearly')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -90,11 +86,13 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
icon={<FaMinus className="w-4 h-4" />}
|
icon={<FaMinus className="w-4 h-4" />}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="xs"
|
size="xs"
|
||||||
title={hasBasketItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
title={hasBasketItems ? translate('::Public.basket.periodLocked') : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 bg-gray-50 px-2 py-1 rounded-lg dark:bg-gray-800">
|
<div className="flex items-center space-x-2 bg-gray-50 px-2 py-1 rounded-lg dark:bg-gray-800">
|
||||||
<span className="font-bold text-lg text-gray-600 dark:text-white">{globalPeriod}</span>
|
<span className="font-bold text-lg text-gray-600 dark:text-white">
|
||||||
|
{globalPeriod}
|
||||||
|
</span>
|
||||||
<span className="text-xs text-gray-600 dark:text-white">
|
<span className="text-xs text-gray-600 dark:text-white">
|
||||||
{globalBillingCycle === 'monthly'
|
{globalBillingCycle === 'monthly'
|
||||||
? translate('::Public.products.billingcycle.month')
|
? translate('::Public.products.billingcycle.month')
|
||||||
|
|
@ -108,7 +106,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
icon={<FaPlus className="w-4 h-4" />}
|
icon={<FaPlus className="w-4 h-4" />}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="xs"
|
size="xs"
|
||||||
title={hasBasketItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
title={hasBasketItems ? translate('::Public.basket.periodLocked') : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FaArrowLeft, FaBuilding, FaCalendarAlt, FaCheckCircle, FaEnvelope, FaPhone } from 'react-icons/fa'
|
import {
|
||||||
|
FaArrowLeft,
|
||||||
|
FaBuilding,
|
||||||
|
FaCalendarAlt,
|
||||||
|
FaCheckCircle,
|
||||||
|
FaEnvelope,
|
||||||
|
FaPhone,
|
||||||
|
} from 'react-icons/fa'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
@ -35,15 +42,15 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
|
||||||
|
|
||||||
const billingCycleLabel =
|
const billingCycleLabel =
|
||||||
order?.billingCycle === 'monthly'
|
order?.billingCycle === 'monthly'
|
||||||
? translate('::Public.products.billingcycle.monthly')
|
? translate('::Public.products.billingcycle.monthly')
|
||||||
: order?.billingCycle === 'yearly'
|
: order?.billingCycle === 'yearly'
|
||||||
? translate('::Public.products.billingcycle.yearly')
|
? translate('::Public.products.billingcycle.yearly')
|
||||||
: '-'
|
: '-'
|
||||||
|
|
||||||
const getItemPrice = (item: BasketItem) =>
|
const getItemPrice = (item: BasketItem) =>
|
||||||
item.billingCycle === 'monthly'
|
item.billingCycle === 'monthly'
|
||||||
? item.product.monthlyPrice ?? 0
|
? (item.product.monthlyPrice ?? 0)
|
||||||
: item.product.yearlyPrice ?? 0
|
: (item.product.yearlyPrice ?? 0)
|
||||||
|
|
||||||
const vatRateLabel = (() => {
|
const vatRateLabel = (() => {
|
||||||
if (!order) return ''
|
if (!order) return ''
|
||||||
|
|
@ -117,7 +124,9 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300 sm:grid-cols-2">
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
|
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">License Start Time</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
License Start Time
|
||||||
|
</div>
|
||||||
<div className="font-semibold">{formatDate(order.licenseStartTime)}</div>
|
<div className="font-semibold">{formatDate(order.licenseStartTime)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
|
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
|
||||||
|
|
@ -158,7 +167,11 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
|
||||||
<div className="col-span-1 text-right">{order.period}</div>
|
<div className="col-span-1 text-right">{order.period}</div>
|
||||||
<div className="col-span-1 text-right">{item.quantity}</div>
|
<div className="col-span-1 text-right">{item.quantity}</div>
|
||||||
<div className="col-span-1 text-right">
|
<div className="col-span-1 text-right">
|
||||||
{item.billingCycle === 'monthly' ? 'Aylık' : 'Yıllık'}
|
{translate(
|
||||||
|
item.billingCycle === 'monthly'
|
||||||
|
? '::Public.order.monthly'
|
||||||
|
: '::Public.order.yearly',
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 text-right">
|
<div className="col-span-2 text-right">
|
||||||
%{((item.vatRate ?? 0) * 100).toFixed(0)} / {formatPrice(item.vatAmount ?? 0)}
|
%{((item.vatRate ?? 0) * 100).toFixed(0)} / {formatPrice(item.vatAmount ?? 0)}
|
||||||
|
|
@ -177,7 +190,10 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) =>
|
||||||
<span>{formatPrice(order.subtotal)}</span>
|
<span>{formatPrice(order.subtotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span>
|
<span>
|
||||||
|
{translate('::Public.products.kdv')}
|
||||||
|
{vatRateLabel}
|
||||||
|
</span>
|
||||||
<span>{formatPrice(order.vatTotal)}</span>
|
<span>{formatPrice(order.vatTotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ interface PaymentFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, basketData }) => {
|
export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, basketData }) => {
|
||||||
const defaultPaymentMethod = 'Kredi Kartı'
|
const defaultPaymentMethod = 'CreditCard'
|
||||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod)
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod)
|
||||||
const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>()
|
const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>()
|
||||||
const [paymentData, setPaymentData] = useState({
|
const [paymentData, setPaymentData] = useState({
|
||||||
|
|
@ -390,7 +390,10 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, ba
|
||||||
<span>{formatPrice(basketData.subtotal)}</span>
|
<span>{formatPrice(basketData.subtotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span>
|
<span>
|
||||||
|
{translate('::Public.products.kdv')}
|
||||||
|
{vatRateLabel}
|
||||||
|
</span>
|
||||||
<span>{formatPrice(basketData.vatTotal)}</span>
|
<span>{formatPrice(basketData.vatTotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 dark:text-gray-100">{translate('::' + product.name)}</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 dark:text-gray-100">
|
||||||
|
{translate('::' + product.name)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">{translate('::' + product.description)}</p>
|
<p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">
|
||||||
|
{translate('::' + product.description)}
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Quantity and Yearly Savings above price */}
|
{/* Quantity and Yearly Savings above price */}
|
||||||
<div className="mb-4 space-y-3">
|
<div className="mb-4 space-y-3">
|
||||||
|
|
@ -157,15 +161,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
{formatPrice(getCurrentPrice())}
|
{formatPrice(getCurrentPrice())}
|
||||||
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getUnitText()}</span>
|
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
|
||||||
|
{getUnitText()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{translate('::Public.products.kdv')} (%{(getVatRate() * 100).toFixed(0)}): {formatPrice(getVatAmount())}
|
{translate('::Public.products.kdv')} (%{(getVatRate() * 100).toFixed(0)}):{' '}
|
||||||
|
{formatPrice(getVatAmount())}
|
||||||
</div>
|
</div>
|
||||||
{globalPeriod > 1 && (
|
{globalPeriod > 1 && (
|
||||||
<div className="text-lg font-semibold text-blue-600 mt-1">
|
<div className="text-lg font-semibold text-blue-600 mt-1">
|
||||||
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPriceWithVat())}
|
{translate('::App.Listform.ListformField.Total')}{' '}
|
||||||
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getPeriodText()}</span>
|
{formatPrice(getTotalPriceWithVat())}
|
||||||
|
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
|
||||||
|
{getPeriodText()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{globalPeriod <= 1 && (
|
{globalPeriod <= 1 && (
|
||||||
|
|
@ -188,7 +198,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
size="md"
|
size="md"
|
||||||
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
|
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
|
||||||
>
|
>
|
||||||
{isDisabled ? translate('::Public.products.inBasket') : translate('::Public.products.addToBasket')}
|
{isDisabled
|
||||||
|
? translate('::Public.products.inBasket')
|
||||||
|
: translate('::Public.products.addToBasket')}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,12 @@ const getNationalPhoneDigits = (value: string, country?: CountryDto) => {
|
||||||
return digits
|
return digits
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPhoneValidationError = (value: string, country?: CountryDto) => {
|
const getPhoneValidationError = (
|
||||||
if (!country) return 'Lütfen önce ülke seçin.'
|
value: string,
|
||||||
|
country: CountryDto | undefined,
|
||||||
|
translate: (key: string, params?: Record<string, string | number>) => string,
|
||||||
|
) => {
|
||||||
|
if (!country) return translate('::App.Tenant.SelectCountryFirst')
|
||||||
|
|
||||||
const length = getNationalPhoneDigits(value, country).length
|
const length = getNationalPhoneDigits(value, country).length
|
||||||
const minLength = getPhoneMinLength(country)
|
const minLength = getPhoneMinLength(country)
|
||||||
|
|
@ -100,8 +104,8 @@ const getPhoneValidationError = (value: string, country?: CountryDto) => {
|
||||||
if (length >= minLength && length <= maxLength) return ''
|
if (length >= minLength && length <= maxLength) return ''
|
||||||
|
|
||||||
return minLength === maxLength
|
return minLength === maxLength
|
||||||
? `Telefon numarası ${minLength} haneli olmalıdır.`
|
? translate('::App.Tenant.PhoneExactLength', { length: minLength })
|
||||||
: `Telefon numarası ${minLength}-${maxLength} hane arasında olmalıdır.`
|
: translate('::App.Tenant.PhoneRangeLength', { min: minLength, max: maxLength })
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatPhoneNumber = (value: string, country?: CountryDto) => {
|
const formatPhoneNumber = (value: string, country?: CountryDto) => {
|
||||||
|
|
@ -309,7 +313,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
setExistingTenantError('Lütfen önce organization code girin.')
|
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,12 +322,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
|
normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
|
||||||
!formData.organizationName
|
!formData.organizationName
|
||||||
) {
|
) {
|
||||||
setExistingTenantError('Devam etmek için önce organization code ile kurum bilgisini bulun.')
|
setExistingTenantError(translate('::App.Tenant.FindOrgBeforeContinue'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) {
|
if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) {
|
||||||
setExistingTenantError('Kurum bilgileri eksik geldi. Lütfen kurum kodunu tekrar aratın.')
|
setExistingTenantError(translate('::App.Tenant.OrgDataIncomplete'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,23 +342,27 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
|
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
|
||||||
setFormError('Lütfen zorunlu alanları eksiksiz doldurun.')
|
setFormError(translate('::App.Tenant.FillRequiredFields'))
|
||||||
hasValidationError = true
|
hasValidationError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPhoneError = getPhoneValidationError(formData.phoneNumber || '', selectedCountry)
|
const nextPhoneError = getPhoneValidationError(
|
||||||
|
formData.phoneNumber || '',
|
||||||
|
selectedCountry,
|
||||||
|
translate,
|
||||||
|
)
|
||||||
setPhoneError(nextPhoneError)
|
setPhoneError(nextPhoneError)
|
||||||
if (nextPhoneError) {
|
if (nextPhoneError) {
|
||||||
hasValidationError = true
|
hasValidationError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
|
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
|
||||||
setEmailError('Lütfen geçerli bir e-posta adresi girin.')
|
setEmailError(translate('::App.Platform.InvalidEmail'))
|
||||||
hasValidationError = true
|
hasValidationError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) {
|
if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) {
|
||||||
setWebsiteError('Lütfen geçerli bir web sitesi adresi girin.')
|
setWebsiteError(translate('::App.Tenant.InvalidWebsite'))
|
||||||
hasValidationError = true
|
hasValidationError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,7 +507,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
const getTenantInfo = async () => {
|
const getTenantInfo = async () => {
|
||||||
const tenantName = formData.name?.trim()
|
const tenantName = formData.name?.trim()
|
||||||
if (!tenantName) {
|
if (!tenantName) {
|
||||||
setExistingTenantError('Lütfen önce organization code girin.')
|
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,7 +543,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
if (tenant.data.name) setFoundTenantName(tenant.data.name)
|
if (tenant.data.name) setFoundTenantName(tenant.data.name)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFoundTenantName('')
|
setFoundTenantName('')
|
||||||
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.')
|
setExistingTenantError(translate('::App.Tenant.OrgNotFound'))
|
||||||
console.error('Kurum bilgisi alınırken hata:', error)
|
console.error('Kurum bilgisi alınırken hata:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingTenant(false)
|
setIsLoadingTenant(false)
|
||||||
|
|
@ -634,7 +642,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2"
|
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2"
|
||||||
>
|
>
|
||||||
{isLoadingTenant
|
{isLoadingTenant
|
||||||
? 'Aranıyor...'
|
? translate('::App.Tenant.Searching')
|
||||||
: translate('::Public.products.tenantForm.searchOrg')}
|
: translate('::Public.products.tenantForm.searchOrg')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -647,7 +655,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
!canSubmitExistingTenant &&
|
!canSubmitExistingTenant &&
|
||||||
!isLoadingTenant && (
|
!isLoadingTenant && (
|
||||||
<p className="mt-2 text-sm text-amber-600">
|
<p className="mt-2 text-sm text-amber-600">
|
||||||
Devam etmek için önce Find Organization ile kurum bilgilerini getirin.
|
{translate('::App.Tenant.FindOrgHint')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -807,7 +815,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
countryOptions.find((option) => option.value === formData.country) || null
|
countryOptions.find((option) => option.value === formData.country) || null
|
||||||
}
|
}
|
||||||
onChange={handleCountryChange}
|
onChange={handleCountryChange}
|
||||||
placeholder="Ülke seçin"
|
placeholder={translate('::App.Tenant.SelectCountry')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -827,7 +835,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
options={cityOptions}
|
options={cityOptions}
|
||||||
value={cityOptions.find((option) => option.value === formData.city) || null}
|
value={cityOptions.find((option) => option.value === formData.city) || null}
|
||||||
onChange={handleCityChange}
|
onChange={handleCityChange}
|
||||||
placeholder={formData.country ? 'Şehir seçin' : 'Önce ülke seçin'}
|
placeholder={translate(
|
||||||
|
formData.country
|
||||||
|
? '::App.Tenant.SelectCity'
|
||||||
|
: '::App.Tenant.SelectCountryFirstShort',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -847,7 +859,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
onChange={handleDistrictChange}
|
onChange={handleDistrictChange}
|
||||||
placeholder={formData.city ? 'İlçe seçin' : 'Önce şehir seçin'}
|
placeholder={translate(
|
||||||
|
formData.city
|
||||||
|
? '::App.Tenant.SelectDistrict'
|
||||||
|
: '::App.Tenant.SelectCityFirst',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -870,7 +886,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
) || null
|
) || null
|
||||||
}
|
}
|
||||||
onChange={handleTownshipChange}
|
onChange={handleTownshipChange}
|
||||||
placeholder={formData.district ? 'Township seçin' : 'Önce ilçe seçin'}
|
placeholder={translate(
|
||||||
|
formData.district
|
||||||
|
? '::App.Tenant.SelectTownship'
|
||||||
|
: '::App.Tenant.SelectDistrictFirst',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -922,7 +942,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
required
|
required
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
placeholder={phoneMask.replace(/0/g, '_')}
|
placeholder={phoneMask.replace(/0/g, '_')}
|
||||||
aria-label={`Telefon numarası, format: ${phoneMask}`}
|
aria-label={translate('::App.Tenant.PhoneAria', { mask: phoneMask })}
|
||||||
value={formData.phoneNumber || ''}
|
value={formData.phoneNumber || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
|
|
@ -933,7 +953,11 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
}}
|
}}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
setPhoneError(
|
setPhoneError(
|
||||||
getPhoneValidationError(formData.phoneNumber || '', selectedCountry),
|
getPhoneValidationError(
|
||||||
|
formData.phoneNumber || '',
|
||||||
|
selectedCountry,
|
||||||
|
translate,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
aria-invalid={Boolean(phoneError)}
|
aria-invalid={Boolean(phoneError)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import Editor, { type Monaco } from '@monaco-editor/react'
|
import Editor, { type Monaco } from '@monaco-editor/react'
|
||||||
import type * as monacoApi from 'monaco-editor'
|
import type * as monacoApi from 'monaco-editor'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
@ -69,9 +70,12 @@ function ScriptBuilderDialog({
|
||||||
dialect,
|
dialect,
|
||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
saveLabel = 'Kaydet',
|
saveLabel,
|
||||||
cancelLabel = 'Vazgeç',
|
cancelLabel,
|
||||||
}: ScriptBuilderDialogProps) {
|
}: ScriptBuilderDialogProps) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const saveText = saveLabel ?? translate('::Save')
|
||||||
|
const cancelText = cancelLabel ?? translate('::Cancel')
|
||||||
const [rules, setRules] = useState<ScriptRule[]>([])
|
const [rules, setRules] = useState<ScriptRule[]>([])
|
||||||
const [code, setCode] = useState('')
|
const [code, setCode] = useState('')
|
||||||
// Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür.
|
// Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür.
|
||||||
|
|
@ -169,7 +173,11 @@ function ScriptBuilderDialog({
|
||||||
* Ek koşul kalmadığında `conditions` alanı tamamen kaldırılır — eski tek
|
* Ek koşul kalmadığında `conditions` alanı tamamen kaldırılır — eski tek
|
||||||
* koşullu kurallarla aynı JSON'a serileşmesi buna bağlı.
|
* koşullu kurallarla aynı JSON'a serileşmesi buna bağlı.
|
||||||
*/
|
*/
|
||||||
const updateCondition = (rule: ScriptRule, index: number, patch: Partial<ScriptRuleCondition>) => {
|
const updateCondition = (
|
||||||
|
rule: ScriptRule,
|
||||||
|
index: number,
|
||||||
|
patch: Partial<ScriptRuleCondition>,
|
||||||
|
) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
updateRule(rule.id, { condition: { ...rule.condition, ...patch } })
|
updateRule(rule.id, { condition: { ...rule.condition, ...patch } })
|
||||||
return
|
return
|
||||||
|
|
@ -191,7 +199,10 @@ function ScriptBuilderDialog({
|
||||||
|
|
||||||
const removeCondition = (rule: ScriptRule, index: number) => {
|
const removeCondition = (rule: ScriptRule, index: number) => {
|
||||||
const conditions = (rule.conditions ?? []).filter((_, item) => item !== index - 1)
|
const conditions = (rule.conditions ?? []).filter((_, item) => item !== index - 1)
|
||||||
updateRule(rule.id, conditions.length ? { conditions } : { conditions: undefined, join: undefined })
|
updateRule(
|
||||||
|
rule.id,
|
||||||
|
conditions.length ? { conditions } : { conditions: undefined, join: undefined },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveRule = (index: number, delta: number) =>
|
const moveRule = (index: number, delta: number) =>
|
||||||
|
|
@ -314,7 +325,7 @@ function ScriptBuilderDialog({
|
||||||
updateRule(rule.id, { fields: [...selectedFields, next] })
|
updateRule(rule.id, { fields: [...selectedFields, next] })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">+ ekle</option>
|
<option value="">{translate('::App.ScriptBuilder.AddOption')}</option>
|
||||||
{options
|
{options
|
||||||
.filter((fieldName) => !selectedFields.includes(fieldName))
|
.filter((fieldName) => !selectedFields.includes(fieldName))
|
||||||
.map((fieldName) => (
|
.map((fieldName) => (
|
||||||
|
|
@ -341,7 +352,7 @@ function ScriptBuilderDialog({
|
||||||
param.optionSource,
|
param.optionSource,
|
||||||
currentValue,
|
currentValue,
|
||||||
(next) => updateParam(rule.id, param.key, next),
|
(next) => updateParam(rule.id, param.key, next),
|
||||||
param.placeholder || 'Seç',
|
param.placeholder || translate('::App.ScriptBuilder.Choose'),
|
||||||
)}
|
)}
|
||||||
{param.type === 'select' && (
|
{param.type === 'select' && (
|
||||||
<select
|
<select
|
||||||
|
|
@ -384,7 +395,9 @@ function ScriptBuilderDialog({
|
||||||
<div key={index} className="grid grid-cols-12 items-end gap-2">
|
<div key={index} className="grid grid-cols-12 items-end gap-2">
|
||||||
{index > 0 ? (
|
{index > 0 ? (
|
||||||
<label className="col-span-12 min-w-0 md:col-span-2">
|
<label className="col-span-12 min-w-0 md:col-span-2">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Bağlaç</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
|
{translate('::App.ScriptBuilder.Conjunction')}
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={rule.join ?? 'and'}
|
value={rule.join ?? 'and'}
|
||||||
|
|
@ -392,19 +405,25 @@ function ScriptBuilderDialog({
|
||||||
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] })
|
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="and">VE</option>
|
<option value="and">{translate('::App.ScriptBuilder.And')}</option>
|
||||||
<option value="or">VEYA</option>
|
<option value="or">{translate('::App.ScriptBuilder.Or')}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-12 hidden md:col-span-2 md:block">
|
<div className="col-span-12 hidden md:col-span-2 md:block">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Koşul</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
<div className="flex h-9 items-center text-xs font-semibold text-gray-400">EĞER</div>
|
{translate('::App.ScriptBuilder.Condition')}
|
||||||
|
</span>
|
||||||
|
<div className="flex h-9 items-center text-xs font-semibold text-gray-400">
|
||||||
|
{translate('::App.ScriptBuilder.If')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="col-span-12 min-w-0 md:col-span-3">
|
<label className="col-span-12 min-w-0 md:col-span-3">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Karşılaştırma</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
|
{translate('::App.ScriptBuilder.Comparison')}
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={condition.operator}
|
value={condition.operator}
|
||||||
|
|
@ -412,7 +431,7 @@ function ScriptBuilderDialog({
|
||||||
>
|
>
|
||||||
{dialect.operators.map((item) => (
|
{dialect.operators.map((item) => (
|
||||||
<option key={item.value} value={item.value}>
|
<option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label.startsWith('App.') ? translate('::' + item.label) : item.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -420,7 +439,9 @@ function ScriptBuilderDialog({
|
||||||
|
|
||||||
{operator?.needsSource && dialect.conditionKinds.length > 1 && (
|
{operator?.needsSource && dialect.conditionKinds.length > 1 && (
|
||||||
<label className="col-span-12 min-w-0 md:col-span-2">
|
<label className="col-span-12 min-w-0 md:col-span-2">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Kaynak</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
|
{translate('::App.ScriptBuilder.Source')}
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''}
|
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''}
|
||||||
|
|
@ -439,19 +460,23 @@ function ScriptBuilderDialog({
|
||||||
|
|
||||||
{operator?.needsSource && (
|
{operator?.needsSource && (
|
||||||
<label className="col-span-12 min-w-0 md:col-span-3">
|
<label className="col-span-12 min-w-0 md:col-span-3">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Alan</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
|
{translate('::App.ScriptBuilder.Field')}
|
||||||
|
</span>
|
||||||
{renderOptionInput(
|
{renderOptionInput(
|
||||||
kind?.optionSource,
|
kind?.optionSource,
|
||||||
condition.source,
|
condition.source,
|
||||||
(next) => updateCondition(rule, index, { source: next }),
|
(next) => updateCondition(rule, index, { source: next }),
|
||||||
kind?.placeholder || 'Seç',
|
kind?.placeholder || translate('::App.ScriptBuilder.Choose'),
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{operator?.needsValue && (
|
{operator?.needsValue && (
|
||||||
<label className="col-span-12 min-w-0 md:col-span-2">
|
<label className="col-span-12 min-w-0 md:col-span-2">
|
||||||
<span className="mb-1 block text-xs text-gray-500">Değer</span>
|
<span className="mb-1 block text-xs text-gray-500">
|
||||||
|
{translate('::App.ScriptBuilder.Value')}
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={condition.value}
|
value={condition.value}
|
||||||
|
|
@ -468,7 +493,7 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Koşulu kaldır"
|
title={translate('::App.ScriptBuilder.RemoveCondition')}
|
||||||
icon={<FaTimes />}
|
icon={<FaTimes />}
|
||||||
onClick={() => removeCondition(rule, index)}
|
onClick={() => removeCondition(rule, index)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -511,7 +536,8 @@ function ScriptBuilderDialog({
|
||||||
</div>
|
</div>
|
||||||
{conditions.length > 1 && (
|
{conditions.length > 1 && (
|
||||||
<span className="shrink-0 rounded-full bg-violet-100 px-2 py-[1px] text-[11px] text-violet-700 dark:bg-violet-900 dark:text-violet-200">
|
<span className="shrink-0 rounded-full bg-violet-100 px-2 py-[1px] text-[11px] text-violet-700 dark:bg-violet-900 dark:text-violet-200">
|
||||||
{conditions.length} koşul · {rule.join === 'or' ? 'VEYA' : 'VE'}
|
{translate('::App.ScriptBuilder.ConditionCount', { count: conditions.length })} ·{' '}
|
||||||
|
{translate(rule.join === 'or' ? '::App.ScriptBuilder.Or' : '::App.ScriptBuilder.And')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{triggerInfo && trigger !== dialect.triggers?.[0]?.value && (
|
{triggerInfo && trigger !== dialect.triggers?.[0]?.value && (
|
||||||
|
|
@ -527,10 +553,10 @@ function ScriptBuilderDialog({
|
||||||
className="flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-[1px] text-[11px] text-amber-700 dark:bg-amber-900 dark:text-amber-200"
|
className="flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-[1px] text-[11px] text-amber-700 dark:bg-amber-900 dark:text-amber-200"
|
||||||
title={
|
title={
|
||||||
dialect.getRuleWarning?.(rule) ||
|
dialect.getRuleWarning?.(rule) ||
|
||||||
'Zorunlu alanlar dolmadığı için bu kural script’e eklenmez.'
|
translate('::App.ScriptBuilder.IncompleteRuleWarning')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaExclamationTriangle size={9} /> eksik
|
<FaExclamationTriangle size={9} /> {translate('::App.ScriptBuilder.Missing')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex shrink-0 items-center">
|
<div className="flex shrink-0 items-center">
|
||||||
|
|
@ -539,7 +565,7 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Yukarı taşı"
|
title={translate('::App.Platform.MoveUp')}
|
||||||
icon={<FaArrowUp />}
|
icon={<FaArrowUp />}
|
||||||
onClick={() => moveRule(index, -1)}
|
onClick={() => moveRule(index, -1)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -548,7 +574,7 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Aşağı taşı"
|
title={translate('::App.Platform.MoveDown')}
|
||||||
icon={<FaArrowDown />}
|
icon={<FaArrowDown />}
|
||||||
onClick={() => moveRule(index, 1)}
|
onClick={() => moveRule(index, 1)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -557,7 +583,7 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Kuralı sil"
|
title={translate('::App.ScriptBuilder.DeleteRule')}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
onClick={() => touchRules((current) => current.filter((item) => item.id !== rule.id))}
|
onClick={() => touchRules((current) => current.filter((item) => item.id !== rule.id))}
|
||||||
/>
|
/>
|
||||||
|
|
@ -595,10 +621,10 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
title="Aynı kurala ikinci bir koşul ekler; VE/VEYA ile birleşir."
|
title={translate('::App.ScriptBuilder.AddConditionHint')}
|
||||||
onClick={() => addCondition(rule)}
|
onClick={() => addCondition(rule)}
|
||||||
>
|
>
|
||||||
Koşul ekle
|
{translate('::App.ScriptBuilder.AddCondition')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -622,7 +648,7 @@ function ScriptBuilderDialog({
|
||||||
<FaSearch className="text-gray-400" size={11} />
|
<FaSearch className="text-gray-400" size={11} />
|
||||||
<input
|
<input
|
||||||
className="w-full min-w-0 bg-transparent text-xs text-gray-700 outline-none dark:text-gray-100"
|
className="w-full min-w-0 bg-transparent text-xs text-gray-700 outline-none dark:text-gray-100"
|
||||||
placeholder="Kısayol veya snippet ara"
|
placeholder={translate('::App.ScriptBuilder.SearchHelp')}
|
||||||
value={helpSearch}
|
value={helpSearch}
|
||||||
onChange={(event) => setHelpSearch(event.target.value)}
|
onChange={(event) => setHelpSearch(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -631,7 +657,7 @@ function ScriptBuilderDialog({
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Paneli kapat"
|
title={translate('::App.Platform.ClosePanel')}
|
||||||
icon={<FaTimes />}
|
icon={<FaTimes />}
|
||||||
onClick={() => setHelpOpen(false)}
|
onClick={() => setHelpOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -656,7 +682,9 @@ function ScriptBuilderDialog({
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{!filteredSnippets.length && (
|
{!filteredSnippets.length && (
|
||||||
<span className="text-[11px] text-gray-400">Eşleşen snippet yok.</span>
|
<span className="text-[11px] text-gray-400">
|
||||||
|
{translate('::App.ScriptBuilder.NoMatchingSnippet')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
|
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
|
||||||
|
|
@ -678,7 +706,9 @@ function ScriptBuilderDialog({
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{!filteredApi.length && (
|
{!filteredApi.length && (
|
||||||
<span className="text-[11px] text-gray-400">Eşleşen kısayol yok.</span>
|
<span className="text-[11px] text-gray-400">
|
||||||
|
{translate('::App.ScriptBuilder.NoMatchingShortcut')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -728,15 +758,15 @@ function ScriptBuilderDialog({
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
title="Kısayol ve snippet panelini aç"
|
title={translate('::App.ScriptBuilder.OpenHelpPanel')}
|
||||||
icon={<FaBook />}
|
icon={<FaBook />}
|
||||||
onClick={() => setHelpOpen(true)}
|
onClick={() => setHelpOpen(true)}
|
||||||
>
|
>
|
||||||
Yardım
|
{translate('::App.ScriptBuilder.Help')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyScript}>
|
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyScript}>
|
||||||
{copied ? 'Kopyalandı' : 'Kopyala'}
|
{translate(copied ? '::App.Platform.Copied' : '::App.SqlQueryManager.Copy')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
|
<div className="flex overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<button
|
||||||
|
|
@ -757,10 +787,10 @@ function ScriptBuilderDialog({
|
||||||
? 'bg-indigo-500 text-white'
|
? 'bg-indigo-500 text-white'
|
||||||
: 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
|
: 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
}`}
|
}`}
|
||||||
title="Kod yazmadan kural kurmak için sihirbazı aç"
|
title={translate('::App.ScriptBuilder.OpenWizardHint')}
|
||||||
onClick={openWizard}
|
onClick={openWizard}
|
||||||
>
|
>
|
||||||
<FaMagic /> Sihirbaz
|
<FaMagic /> {translate('::App.ScriptBuilder.Wizard')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -769,12 +799,9 @@ function ScriptBuilderDialog({
|
||||||
{view === 'wizard' && codeDiverged && (
|
{view === 'wizard' && codeDiverged && (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
|
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
|
||||||
<FaExclamationTriangle className="shrink-0" />
|
<FaExclamationTriangle className="shrink-0" />
|
||||||
<span className="flex-1">
|
<span className="flex-1">{translate('::App.ScriptBuilder.CodeDivergedWarning')}</span>
|
||||||
Koddaki script kurallardan farklı. Burada bir kuralı değiştirdiğin anda kod
|
|
||||||
kurallardan yeniden üretilir.
|
|
||||||
</span>
|
|
||||||
<Button size="xs" type="button" onClick={() => setView('code')}>
|
<Button size="xs" type="button" onClick={() => setView('code')}>
|
||||||
Kodu gör
|
{translate('::App.ScriptBuilder.ShowCode')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -789,11 +816,11 @@ function ScriptBuilderDialog({
|
||||||
disabled={!generatedScript}
|
disabled={!generatedScript}
|
||||||
size="xs"
|
size="xs"
|
||||||
type="button"
|
type="button"
|
||||||
title="Sihirbazdaki kurallardan script'i yeniden üretir."
|
title={translate('::App.ScriptBuilder.RegenerateHint')}
|
||||||
icon={<FaMagic />}
|
icon={<FaMagic />}
|
||||||
onClick={() => setCode(generatedScript)}
|
onClick={() => setCode(generatedScript)}
|
||||||
>
|
>
|
||||||
Kurallardan üret
|
{translate('::App.ScriptBuilder.Regenerate')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -824,7 +851,9 @@ function ScriptBuilderDialog({
|
||||||
{paletteOpen ? <FaChevronDown size={11} /> : <FaChevronRight size={11} />}
|
{paletteOpen ? <FaChevronDown size={11} /> : <FaChevronRight size={11} />}
|
||||||
</span>
|
</span>
|
||||||
<FaPlus className="text-indigo-500" size={12} />
|
<FaPlus className="text-indigo-500" size={12} />
|
||||||
<span className="flex-1 text-sm font-semibold">Kural Ekle</span>
|
<span className="flex-1 text-sm font-semibold">
|
||||||
|
{translate('::App.ScriptBuilder.AddRule')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{paletteOpen && (
|
{paletteOpen && (
|
||||||
<div className="flex flex-col gap-3 border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
<div className="flex flex-col gap-3 border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
||||||
|
|
@ -848,7 +877,7 @@ function ScriptBuilderDialog({
|
||||||
key={recipe.id}
|
key={recipe.id}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
title={`${recipe.summary}\n\nÖrnek: ${recipe.example}`}
|
title={`${recipe.summary}\n\n${translate('::App.ScriptBuilder.Example')}: ${recipe.example}`}
|
||||||
onClick={() => addRule(recipe.id)}
|
onClick={() => addRule(recipe.id)}
|
||||||
>
|
>
|
||||||
{recipe.label}
|
{recipe.label}
|
||||||
|
|
@ -866,8 +895,7 @@ function ScriptBuilderDialog({
|
||||||
|
|
||||||
{!rules.length && (
|
{!rules.length && (
|
||||||
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
|
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
|
||||||
Henüz kural yok. Yukarıdan bir kural ekle; her kural tek satırlık script üretir ve
|
{translate('::App.ScriptBuilder.NoRulesYet')}
|
||||||
kod sekmesine anında yansır.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -876,10 +904,14 @@ function ScriptBuilderDialog({
|
||||||
<div className="mb-3 flex items-center justify-between gap-2">
|
<div className="mb-3 flex items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2 text-sm font-semibold">
|
<span className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<FaCode className="text-gray-400" />
|
<FaCode className="text-gray-400" />
|
||||||
Script Önizleme
|
{translate('::App.ScriptBuilder.ScriptPreview')}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] text-gray-500 dark:bg-gray-800 dark:text-gray-300">
|
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] text-gray-500 dark:bg-gray-800 dark:text-gray-300">
|
||||||
{generatedScript ? `${generatedScript.split('\n').length - 1} satır` : 'boş'}
|
{generatedScript
|
||||||
|
? translate('::App.ScriptBuilder.LineCount', {
|
||||||
|
count: generatedScript.split('\n').length - 1,
|
||||||
|
})
|
||||||
|
: translate('::ListForms.EditorOptions.Empty')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
<div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
|
@ -887,13 +919,16 @@ function ScriptBuilderDialog({
|
||||||
height="100%"
|
height="100%"
|
||||||
language="javascript"
|
language="javascript"
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
value={generatedScript || '// Kural ekledikçe script burada oluşur'}
|
value={
|
||||||
|
generatedScript || `// ${translate('::App.ScriptBuilder.EmptyScriptHint')}`
|
||||||
|
}
|
||||||
options={{ ...editorOptions, readOnly: true }}
|
options={{ ...editorOptions, readOnly: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[11px] text-gray-400">
|
<p className="mt-2 text-[11px] text-gray-400">
|
||||||
İlk satırdaki <code>{'// @builder'}</code> yorumu kuralları saklar; dialog yeniden
|
{translate('::App.ScriptBuilder.BuilderCommentHintPrefix')}{' '}
|
||||||
açıldığında sihirbaz kuralları buradan geri yükler.
|
<code>{'// @builder'}</code>{' '}
|
||||||
|
{translate('::App.ScriptBuilder.BuilderCommentHintSuffix')}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -902,7 +937,7 @@ function ScriptBuilderDialog({
|
||||||
|
|
||||||
<Dialog.Footer className="mt-1 flex justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
<Dialog.Footer className="mt-1 flex justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
||||||
<Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}>
|
<Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}>
|
||||||
{cancelLabel}
|
{cancelText}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -914,7 +949,7 @@ function ScriptBuilderDialog({
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{saveLabel}
|
{saveText}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -161,13 +161,38 @@ export const scriptRuleConditions = (rule: ScriptRule): ScriptRuleCondition[] =>
|
||||||
|
|
||||||
export const DEFAULT_SCRIPT_OPERATORS: ScriptOperator[] = [
|
export const DEFAULT_SCRIPT_OPERATORS: ScriptOperator[] = [
|
||||||
{ value: 'always', label: 'Her zaman' },
|
{ value: 'always', label: 'Her zaman' },
|
||||||
{ value: 'equals', label: 'Eşitse', needsValue: true, needsSource: true },
|
{
|
||||||
{ value: 'notEquals', label: 'Eşit değilse', needsValue: true, needsSource: true },
|
value: 'equals',
|
||||||
{ value: 'contains', label: 'İçeriyorsa', needsValue: true, needsSource: true },
|
label: 'App.ScriptBuilder.Operator.Equals',
|
||||||
{ value: 'empty', label: 'Boşsa', needsSource: true },
|
needsValue: true,
|
||||||
|
needsSource: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'notEquals',
|
||||||
|
label: 'App.ScriptBuilder.Operator.NotEquals',
|
||||||
|
needsValue: true,
|
||||||
|
needsSource: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'contains',
|
||||||
|
label: 'App.ScriptBuilder.Operator.Contains',
|
||||||
|
needsValue: true,
|
||||||
|
needsSource: true,
|
||||||
|
},
|
||||||
|
{ value: 'empty', label: 'App.ScriptBuilder.Operator.Empty', needsSource: true },
|
||||||
{ value: 'notEmpty', label: 'Doluysa', needsSource: true },
|
{ value: 'notEmpty', label: 'Doluysa', needsSource: true },
|
||||||
{ value: 'greaterThan', label: 'Büyükse', needsValue: true, needsSource: true },
|
{
|
||||||
{ value: 'lessThan', label: 'Küçükse', needsValue: true, needsSource: true },
|
value: 'greaterThan',
|
||||||
{ value: 'isTrue', label: 'İşaretliyse', needsSource: true },
|
label: 'App.ScriptBuilder.Operator.GreaterThan',
|
||||||
{ value: 'isFalse', label: 'İşaretli değilse', needsSource: true },
|
needsValue: true,
|
||||||
|
needsSource: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lessThan',
|
||||||
|
label: 'App.ScriptBuilder.Operator.LessThan',
|
||||||
|
needsValue: true,
|
||||||
|
needsSource: true,
|
||||||
|
},
|
||||||
|
{ value: 'isTrue', label: 'App.ScriptBuilder.Operator.IsTrue', needsSource: true },
|
||||||
|
{ value: 'isFalse', label: 'App.ScriptBuilder.Operator.IsFalse', needsSource: true },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'
|
const label = translate('::ListForms.ListForm.DbMigrate')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -39,7 +39,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
|
||||||
icon={<FcAcceptDatabase />}
|
icon={<FcAcceptDatabase />}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={() => setIsConfirmOpen(true)}
|
onClick={() => setIsConfirmOpen(true)}
|
||||||
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
|
title={translate('::App.DbMigrate.StartMessage')}
|
||||||
>
|
>
|
||||||
{hideLabelOnMobile ? <span className="hidden sm:inline">{label}</span> : label}
|
{hideLabelOnMobile ? <span className="hidden sm:inline">{label}</span> : label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -49,7 +49,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
|
||||||
type="info"
|
type="info"
|
||||||
title={label}
|
title={label}
|
||||||
cancelText={translate('::Cancel')}
|
cancelText={translate('::Cancel')}
|
||||||
confirmText={translate('::App.Platform.Execute') || 'Çalıştır'}
|
confirmText={translate('::App.Platform.Execute')}
|
||||||
onCancel={() => setIsConfirmOpen(false)}
|
onCancel={() => setIsConfirmOpen(false)}
|
||||||
onClose={() => setIsConfirmOpen(false)}
|
onClose={() => setIsConfirmOpen(false)}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
|
@ -58,8 +58,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
{translate('::App.DbMigrate.ConfirmMessage') ||
|
{translate('::App.DbMigrate.ConfirmMessage')}
|
||||||
'Are you sure you want to start the database migration process?'}
|
|
||||||
</p>
|
</p>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,7 @@ const TenantSelector = () => {
|
||||||
setTenantName(undefined)
|
setTenantName(undefined)
|
||||||
const parts = window.location.hostname.split('.')
|
const parts = window.location.hostname.split('.')
|
||||||
const mainDomain = parts.length >= 3 ? parts.slice(1).join('.') : window.location.hostname
|
const mainDomain = parts.length >= 3 ? parts.slice(1).join('.') : window.location.hostname
|
||||||
setWarningTimeout(
|
setWarningTimeout(translate('::App.Tenant.NotFoundRedirect', { name }))
|
||||||
`"${name}" kurumuna ait kayıt bulunamadı.\nAna sayfaya yönlendiriliyorsunuz...`,
|
|
||||||
)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `${window.location.protocol}//${mainDomain}`
|
window.location.href = `${window.location.protocol}//${mainDomain}`
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useId,
|
useId,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useConfig } from '../ConfigProvider'
|
import { useConfig } from '../ConfigProvider'
|
||||||
|
|
@ -16,386 +16,348 @@ import { useInputGroup } from '../InputGroup/context'
|
||||||
import { CONTROL_SIZES } from '../utils/constants'
|
import { CONTROL_SIZES } from '../utils/constants'
|
||||||
import { Spinner } from '../Spinner'
|
import { Spinner } from '../Spinner'
|
||||||
import type { CommonProps, TypeAttributes } from '../@types/common'
|
import type { CommonProps, TypeAttributes } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
// ── Tipler ──────────────────────────────────────────────────────────────────
|
// ── Tipler ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface AutoCompleteOption {
|
export interface AutoCompleteOption {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Özel veri (filtreleme / render için) */
|
/** Özel veri (filtreleme / render için) */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data?: any
|
data?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoCompleteProps extends CommonProps {
|
export interface AutoCompleteProps extends CommonProps {
|
||||||
/** Statik seçenek listesi */
|
/** Statik seçenek listesi */
|
||||||
options?: AutoCompleteOption[]
|
options?: AutoCompleteOption[]
|
||||||
/** Kontrollü input değeri */
|
/** Kontrollü input değeri */
|
||||||
value?: string
|
value?: string
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
/** Placeholder */
|
/** Placeholder */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Geçersiz (kırmızı kenarlık) */
|
/** Geçersiz (kırmızı kenarlık) */
|
||||||
invalid?: boolean
|
invalid?: boolean
|
||||||
/** Input boyutu */
|
/** Input boyutu */
|
||||||
size?: TypeAttributes.ControlSize
|
size?: TypeAttributes.ControlSize
|
||||||
/** Async seçenek yükleme - (inputValue) => Promise<AutoCompleteOption[]> */
|
/** Async seçenek yükleme - (inputValue) => Promise<AutoCompleteOption[]> */
|
||||||
fetchOptions?: (query: string) => Promise<AutoCompleteOption[]>
|
fetchOptions?: (query: string) => Promise<AutoCompleteOption[]>
|
||||||
/** Async istekleri geciktir (ms). Varsayılan: 300 */
|
/** Async istekleri geciktir (ms). Varsayılan: 300 */
|
||||||
debounce?: number
|
debounce?: number
|
||||||
/** Seçenek özelleştirme render */
|
/** Seçenek özelleştirme render */
|
||||||
renderOption?: (option: AutoCompleteOption, active: boolean) => ReactNode
|
renderOption?: (option: AutoCompleteOption, active: boolean) => ReactNode
|
||||||
/** "Seçenek bulunamadı" metni */
|
/** "Seçenek bulunamadı" metni */
|
||||||
noOptionsText?: string
|
noOptionsText?: string
|
||||||
/** Yükleniyor metni */
|
/** Yükleniyor metni */
|
||||||
loadingText?: string
|
loadingText?: string
|
||||||
/** Input değeri değiştiğinde */
|
/** Input değeri değiştiğinde */
|
||||||
onInputChange?: (value: string) => void
|
onInputChange?: (value: string) => void
|
||||||
/** Bir seçenek seçildiğinde */
|
/** Bir seçenek seçildiğinde */
|
||||||
onSelect?: (option: AutoCompleteOption) => void
|
onSelect?: (option: AutoCompleteOption) => void
|
||||||
/** Temizle butonu göster */
|
/** Temizle butonu göster */
|
||||||
clearable?: boolean
|
clearable?: boolean
|
||||||
/** Input adı */
|
/** Input adı */
|
||||||
name?: string
|
name?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
field?: any
|
field?: any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
form?: any
|
form?: any
|
||||||
/** Minimum karakter sayısı (async için). Varsayılan: 1 */
|
/** Minimum karakter sayısı (async için). Varsayılan: 1 */
|
||||||
minChars?: number
|
minChars?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Yardımcılar ─────────────────────────────────────────────────────────────
|
// ── Yardımcılar ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function defaultFilter(options: AutoCompleteOption[], query: string) {
|
function defaultFilter(options: AutoCompleteOption[], query: string) {
|
||||||
const lower = query.toLowerCase()
|
const lower = query.toLowerCase()
|
||||||
return options.filter(
|
return options.filter((o) => !o.disabled && o.label.toLowerCase().includes(lower))
|
||||||
(o) => !o.disabled && o.label.toLowerCase().includes(lower),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDebounce<T extends (...args: Parameters<T>) => void>(
|
function useDebounce<T extends (...args: Parameters<T>) => void>(fn: T, delay: number) {
|
||||||
fn: T,
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
delay: number,
|
return useCallback(
|
||||||
) {
|
(...args: Parameters<T>) => {
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
if (timerRef.current) clearTimeout(timerRef.current)
|
||||||
return useCallback(
|
timerRef.current = setTimeout(() => fn(...args), delay)
|
||||||
(...args: Parameters<T>) => {
|
},
|
||||||
if (timerRef.current) clearTimeout(timerRef.current)
|
[fn, delay],
|
||||||
timerRef.current = setTimeout(() => fn(...args), delay)
|
)
|
||||||
},
|
|
||||||
[fn, delay],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Komponent ────────────────────────────────────────────────────────────────
|
// ── Komponent ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>((props, ref) => {
|
||||||
(props, ref) => {
|
const { translate } = useLocalization()
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
options = [],
|
options = [],
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
defaultValue = '',
|
defaultValue = '',
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
invalid = false,
|
invalid = false,
|
||||||
size,
|
size,
|
||||||
fetchOptions,
|
fetchOptions,
|
||||||
debounce: debounceMs = 300,
|
debounce: debounceMs = 300,
|
||||||
renderOption,
|
renderOption,
|
||||||
noOptionsText = 'Seçenek bulunamadı',
|
noOptionsText,
|
||||||
loadingText = 'Yükleniyor...',
|
loadingText,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
clearable = true,
|
clearable = true,
|
||||||
name,
|
name,
|
||||||
field,
|
field,
|
||||||
form,
|
form,
|
||||||
minChars = 1,
|
minChars = 1,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const isControlled = valueProp !== undefined
|
const isControlled = valueProp !== undefined
|
||||||
const [inputValue, setInputValue] = useState(
|
const [inputValue, setInputValue] = useState(
|
||||||
field?.value ?? (isControlled ? valueProp! : defaultValue),
|
field?.value ?? (isControlled ? valueProp! : defaultValue),
|
||||||
)
|
)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [activeIndex, setActiveIndex] = useState(-1)
|
const [activeIndex, setActiveIndex] = useState(-1)
|
||||||
const [filteredOptions, setFilteredOptions] = useState<
|
const [filteredOptions, setFilteredOptions] = useState<AutoCompleteOption[]>([])
|
||||||
AutoCompleteOption[]
|
const [loading, setLoading] = useState(false)
|
||||||
>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const listRef = useRef<HTMLUListElement>(null)
|
const listRef = useRef<HTMLUListElement>(null)
|
||||||
const listboxId = useId()
|
const listboxId = useId()
|
||||||
|
|
||||||
const { themeColor, primaryColorLevel, controlSize } = useConfig()
|
const { themeColor, primaryColorLevel, controlSize } = useConfig()
|
||||||
const formControlSize = useForm()?.size
|
const formControlSize = useForm()?.size
|
||||||
const inputGroupSize = useInputGroup()?.size
|
const inputGroupSize = useInputGroup()?.size
|
||||||
const resolvedSize =
|
const resolvedSize = size || inputGroupSize || formControlSize || controlSize
|
||||||
size || inputGroupSize || formControlSize || controlSize
|
const sizeClass = `h-${CONTROL_SIZES[resolvedSize]}`
|
||||||
const sizeClass = `h-${CONTROL_SIZES[resolvedSize]}`
|
|
||||||
|
|
||||||
// Sync controlled value
|
// Sync controlled value
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled) setInputValue(valueProp!)
|
if (isControlled) setInputValue(valueProp!)
|
||||||
}, [isControlled, valueProp])
|
}, [isControlled, valueProp])
|
||||||
|
|
||||||
// Sync field value (Formik / RHF)
|
// Sync field value (Formik / RHF)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field?.value !== undefined) setInputValue(field.value)
|
if (field?.value !== undefined) setInputValue(field.value)
|
||||||
}, [field?.value])
|
}, [field?.value])
|
||||||
|
|
||||||
// Dışarı tıkla → kapat
|
// Dışarı tıkla → kapat
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: MouseEvent) => {
|
const handler = (e: MouseEvent) => {
|
||||||
if (
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
containerRef.current &&
|
setOpen(false)
|
||||||
!containerRef.current.contains(e.target as Node)
|
setActiveIndex(-1)
|
||||||
) {
|
}
|
||||||
setOpen(false)
|
}
|
||||||
setActiveIndex(-1)
|
document.addEventListener('mousedown', handler)
|
||||||
}
|
return () => document.removeEventListener('mousedown', handler)
|
||||||
}
|
}, [])
|
||||||
document.addEventListener('mousedown', handler)
|
|
||||||
return () => document.removeEventListener('mousedown', handler)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Statik filtreleme
|
// Statik filtreleme
|
||||||
const filterStatic = useCallback(
|
const filterStatic = useCallback(
|
||||||
(query: string) => {
|
(query: string) => {
|
||||||
if (!fetchOptions) {
|
if (!fetchOptions) {
|
||||||
const result = query ? defaultFilter(options, query) : options
|
const result = query ? defaultFilter(options, query) : options
|
||||||
setFilteredOptions(result)
|
setFilteredOptions(result)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[fetchOptions, options],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Async yükleme (debounced)
|
|
||||||
const fetchAsync = useDebounce(async (query: string) => {
|
|
||||||
if (!fetchOptions) return
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const result = await fetchOptions(query)
|
|
||||||
setFilteredOptions(result)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, debounceMs)
|
|
||||||
|
|
||||||
const openDropdown = useCallback(
|
|
||||||
(query: string) => {
|
|
||||||
if (query.length < minChars && fetchOptions) return
|
|
||||||
setOpen(true)
|
|
||||||
setActiveIndex(-1)
|
|
||||||
if (fetchOptions) {
|
|
||||||
fetchAsync(query)
|
|
||||||
} else {
|
|
||||||
filterStatic(query)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[minChars, fetchOptions, fetchAsync, filterStatic],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const val = e.target.value
|
|
||||||
if (!isControlled) setInputValue(val)
|
|
||||||
field?.onChange?.(val)
|
|
||||||
onInputChange?.(val)
|
|
||||||
openDropdown(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
openDropdown(inputValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitSelection = useCallback(
|
|
||||||
(option: AutoCompleteOption) => {
|
|
||||||
if (!isControlled) setInputValue(option.label)
|
|
||||||
field?.onChange?.(option.label)
|
|
||||||
onInputChange?.(option.label)
|
|
||||||
onSelect?.(option)
|
|
||||||
setOpen(false)
|
|
||||||
setActiveIndex(-1)
|
|
||||||
},
|
|
||||||
[isControlled, field, onInputChange, onSelect],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (!open) {
|
|
||||||
if (e.key === 'ArrowDown') openDropdown(inputValue)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault()
|
|
||||||
setActiveIndex((i) =>
|
|
||||||
Math.min(i + 1, filteredOptions.length - 1),
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault()
|
|
||||||
setActiveIndex((i) => Math.max(i - 1, -1))
|
|
||||||
break
|
|
||||||
case 'Enter':
|
|
||||||
e.preventDefault()
|
|
||||||
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
|
|
||||||
commitSelection(filteredOptions[activeIndex])
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'Escape':
|
|
||||||
setOpen(false)
|
|
||||||
setActiveIndex(-1)
|
|
||||||
break
|
|
||||||
case 'Tab':
|
|
||||||
setOpen(false)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktif öğeyi listede görünür yap
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeIndex < 0 || !listRef.current) return
|
|
||||||
const item = listRef.current.children[activeIndex] as HTMLElement
|
|
||||||
item?.scrollIntoView?.({ block: 'nearest' })
|
|
||||||
}, [activeIndex])
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
if (!isControlled) setInputValue('')
|
|
||||||
field?.onChange?.('')
|
|
||||||
onInputChange?.('')
|
|
||||||
setOpen(false)
|
|
||||||
setFilteredOptions([])
|
|
||||||
}
|
|
||||||
|
|
||||||
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
|
|
||||||
const invalidClass = 'border-red-500 focus:ring-red-500'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className={classNames('autocomplete', className)}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div className="autocomplete-input-wrapper">
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
name={name ?? field?.name}
|
|
||||||
value={inputValue}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder={placeholder}
|
|
||||||
autoComplete="off"
|
|
||||||
className={classNames(
|
|
||||||
'autocomplete-input',
|
|
||||||
sizeClass,
|
|
||||||
invalid ? invalidClass : ringClass,
|
|
||||||
disabled && 'autocomplete-input-disabled',
|
|
||||||
clearable &&
|
|
||||||
inputValue &&
|
|
||||||
'autocomplete-input-clearable',
|
|
||||||
)}
|
|
||||||
role="combobox"
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-expanded={open}
|
|
||||||
aria-controls={listboxId}
|
|
||||||
aria-activedescendant={
|
|
||||||
activeIndex >= 0
|
|
||||||
? `autocomplete-opt-${activeIndex}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
<div className="autocomplete-suffix">
|
|
||||||
{loading && (
|
|
||||||
<Spinner
|
|
||||||
size={14}
|
|
||||||
className="autocomplete-spinner"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{clearable && inputValue && !loading && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="autocomplete-clear"
|
|
||||||
onClick={handleClear}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label="Temizle"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2.5}
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<ul
|
|
||||||
ref={listRef}
|
|
||||||
id={listboxId}
|
|
||||||
role="listbox"
|
|
||||||
className="autocomplete-dropdown"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<li className="autocomplete-option autocomplete-option-info">
|
|
||||||
{loadingText}
|
|
||||||
</li>
|
|
||||||
) : filteredOptions.length === 0 ? (
|
|
||||||
<li className="autocomplete-option autocomplete-option-info">
|
|
||||||
{noOptionsText}
|
|
||||||
</li>
|
|
||||||
) : (
|
|
||||||
filteredOptions.map((option, index) => (
|
|
||||||
<li
|
|
||||||
key={option.value}
|
|
||||||
id={`autocomplete-opt-${index}`}
|
|
||||||
role="option"
|
|
||||||
aria-selected={activeIndex === index}
|
|
||||||
aria-disabled={option.disabled}
|
|
||||||
className={classNames(
|
|
||||||
'autocomplete-option',
|
|
||||||
activeIndex === index &&
|
|
||||||
'autocomplete-option-active',
|
|
||||||
option.disabled &&
|
|
||||||
'autocomplete-option-disabled',
|
|
||||||
)}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={() => {
|
|
||||||
if (!option.disabled)
|
|
||||||
commitSelection(option)
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setActiveIndex(index)}
|
|
||||||
>
|
|
||||||
{renderOption
|
|
||||||
? renderOption(option, activeIndex === index)
|
|
||||||
: option.label}
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
[fetchOptions, options],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Async yükleme (debounced)
|
||||||
|
const fetchAsync = useDebounce(async (query: string) => {
|
||||||
|
if (!fetchOptions) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await fetchOptions(query)
|
||||||
|
setFilteredOptions(result)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, debounceMs)
|
||||||
|
|
||||||
|
const openDropdown = useCallback(
|
||||||
|
(query: string) => {
|
||||||
|
if (query.length < minChars && fetchOptions) return
|
||||||
|
setOpen(true)
|
||||||
|
setActiveIndex(-1)
|
||||||
|
if (fetchOptions) {
|
||||||
|
fetchAsync(query)
|
||||||
|
} else {
|
||||||
|
filterStatic(query)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[minChars, fetchOptions, fetchAsync, filterStatic],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = e.target.value
|
||||||
|
if (!isControlled) setInputValue(val)
|
||||||
|
field?.onChange?.(val)
|
||||||
|
onInputChange?.(val)
|
||||||
|
openDropdown(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
openDropdown(inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitSelection = useCallback(
|
||||||
|
(option: AutoCompleteOption) => {
|
||||||
|
if (!isControlled) setInputValue(option.label)
|
||||||
|
field?.onChange?.(option.label)
|
||||||
|
onInputChange?.(option.label)
|
||||||
|
onSelect?.(option)
|
||||||
|
setOpen(false)
|
||||||
|
setActiveIndex(-1)
|
||||||
|
},
|
||||||
|
[isControlled, field, onInputChange, onSelect],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!open) {
|
||||||
|
if (e.key === 'ArrowDown') openDropdown(inputValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveIndex((i) => Math.min(i + 1, filteredOptions.length - 1))
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveIndex((i) => Math.max(i - 1, -1))
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault()
|
||||||
|
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
|
||||||
|
commitSelection(filteredOptions[activeIndex])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
setOpen(false)
|
||||||
|
setActiveIndex(-1)
|
||||||
|
break
|
||||||
|
case 'Tab':
|
||||||
|
setOpen(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktif öğeyi listede görünür yap
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndex < 0 || !listRef.current) return
|
||||||
|
const item = listRef.current.children[activeIndex] as HTMLElement
|
||||||
|
item?.scrollIntoView?.({ block: 'nearest' })
|
||||||
|
}, [activeIndex])
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
if (!isControlled) setInputValue('')
|
||||||
|
field?.onChange?.('')
|
||||||
|
onInputChange?.('')
|
||||||
|
setOpen(false)
|
||||||
|
setFilteredOptions([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
|
||||||
|
const invalidClass = 'border-red-500 focus:ring-red-500'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className={classNames('autocomplete', className)} style={style}>
|
||||||
|
<div className="autocomplete-input-wrapper">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
name={name ?? field?.name}
|
||||||
|
value={inputValue}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoComplete="off"
|
||||||
|
className={classNames(
|
||||||
|
'autocomplete-input',
|
||||||
|
sizeClass,
|
||||||
|
invalid ? invalidClass : ringClass,
|
||||||
|
disabled && 'autocomplete-input-disabled',
|
||||||
|
clearable && inputValue && 'autocomplete-input-clearable',
|
||||||
|
)}
|
||||||
|
role="combobox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={listboxId}
|
||||||
|
aria-activedescendant={activeIndex >= 0 ? `autocomplete-opt-${activeIndex}` : undefined}
|
||||||
|
onChange={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<div className="autocomplete-suffix">
|
||||||
|
{loading && <Spinner size={14} className="autocomplete-spinner" />}
|
||||||
|
{clearable && inputValue && !loading && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="autocomplete-clear"
|
||||||
|
onClick={handleClear}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={translate('::App.DeveloperKit.CrudEndpoints.Clear')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<ul ref={listRef} id={listboxId} role="listbox" className="autocomplete-dropdown">
|
||||||
|
{loading ? (
|
||||||
|
<li className="autocomplete-option autocomplete-option-info">
|
||||||
|
{loadingText ?? translate('::App.Loading')}
|
||||||
|
</li>
|
||||||
|
) : filteredOptions.length === 0 ? (
|
||||||
|
<li className="autocomplete-option autocomplete-option-info">
|
||||||
|
{noOptionsText ?? translate('::App.AutoComplete.NoOptions')}
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
filteredOptions.map((option, index) => (
|
||||||
|
<li
|
||||||
|
key={option.value}
|
||||||
|
id={`autocomplete-opt-${index}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={activeIndex === index}
|
||||||
|
aria-disabled={option.disabled}
|
||||||
|
className={classNames(
|
||||||
|
'autocomplete-option',
|
||||||
|
activeIndex === index && 'autocomplete-option-active',
|
||||||
|
option.disabled && 'autocomplete-option-disabled',
|
||||||
|
)}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
if (!option.disabled) commitSelection(option)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setActiveIndex(index)}
|
||||||
|
>
|
||||||
|
{renderOption ? renderOption(option, activeIndex === index) : option.label}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
AutoComplete.displayName = 'AutoComplete'
|
AutoComplete.displayName = 'AutoComplete'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,147 @@
|
||||||
import React, {
|
import React, { Children, cloneElement, forwardRef, isValidElement, type ReactNode } from 'react'
|
||||||
Children,
|
|
||||||
cloneElement,
|
|
||||||
forwardRef,
|
|
||||||
isValidElement,
|
|
||||||
type ReactNode,
|
|
||||||
} from 'react'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import type { CommonProps } from '../@types/common'
|
import type { CommonProps } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export interface BreadcrumbItemProps extends CommonProps {
|
export interface BreadcrumbItemProps extends CommonProps {
|
||||||
/** Tıklanabilir link href */
|
/** Tıklanabilir link href */
|
||||||
href?: string
|
href?: string
|
||||||
/** Özel link bileşeni (react-router Link vb.) */
|
/** Özel link bileşeni (react-router Link vb.) */
|
||||||
as?: React.ElementType
|
as?: React.ElementType
|
||||||
/** Aktif (son) öğe mi? Otomatik set edilir */
|
/** Aktif (son) öğe mi? Otomatik set edilir */
|
||||||
active?: boolean
|
active?: boolean
|
||||||
/** İkon (solda) */
|
/** İkon (solda) */
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
/** Tıklama eventi */
|
/** Tıklama eventi */
|
||||||
onClick?: (e: React.MouseEvent) => void
|
onClick?: (e: React.MouseEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BreadcrumbProps extends CommonProps {
|
export interface BreadcrumbProps extends CommonProps {
|
||||||
/** Ayraç. Varsayılan: '/' */
|
/** Ayraç. Varsayılan: '/' */
|
||||||
separator?: ReactNode
|
separator?: ReactNode
|
||||||
/** Öğeler arası boşluk. Varsayılan: 8 */
|
/** Öğeler arası boşluk. Varsayılan: 8 */
|
||||||
gap?: number
|
gap?: number
|
||||||
/** Son öğe hariç metni maxWidth ile kırp */
|
/** Son öğe hariç metni maxWidth ile kırp */
|
||||||
maxItems?: number
|
maxItems?: number
|
||||||
/** Küçük boyut */
|
/** Küçük boyut */
|
||||||
size?: 'sm' | 'md' | 'lg'
|
size?: 'sm' | 'md' | 'lg'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>(
|
export const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>((props, ref) => {
|
||||||
(props, ref) => {
|
const {
|
||||||
const {
|
className,
|
||||||
className,
|
children,
|
||||||
children,
|
style,
|
||||||
style,
|
href,
|
||||||
href,
|
as: Component,
|
||||||
as: Component,
|
active = false,
|
||||||
active = false,
|
icon,
|
||||||
icon,
|
onClick,
|
||||||
onClick,
|
...rest
|
||||||
...rest
|
} = props
|
||||||
} = props
|
|
||||||
|
|
||||||
const Tag = Component ?? (href ? 'a' : 'span')
|
const Tag = Component ?? (href ? 'a' : 'span')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames('breadcrumb-item', active && 'breadcrumb-item-active', className)}
|
||||||
'breadcrumb-item',
|
style={style}
|
||||||
active && 'breadcrumb-item-active',
|
aria-current={active ? 'page' : undefined}
|
||||||
className,
|
{...rest}
|
||||||
)}
|
>
|
||||||
style={style}
|
<Tag
|
||||||
aria-current={active ? 'page' : undefined}
|
href={href}
|
||||||
{...rest}
|
className={classNames(
|
||||||
>
|
'breadcrumb-link',
|
||||||
<Tag
|
active ? 'breadcrumb-link-active' : 'breadcrumb-link-default',
|
||||||
href={href}
|
onClick && !active && 'cursor-pointer',
|
||||||
className={classNames(
|
)}
|
||||||
'breadcrumb-link',
|
onClick={onClick}
|
||||||
active
|
>
|
||||||
? 'breadcrumb-link-active'
|
{icon && <span className="breadcrumb-icon">{icon}</span>}
|
||||||
: 'breadcrumb-link-default',
|
{children}
|
||||||
onClick && !active && 'cursor-pointer',
|
</Tag>
|
||||||
)}
|
</li>
|
||||||
onClick={onClick}
|
)
|
||||||
>
|
})
|
||||||
{icon && (
|
|
||||||
<span className="breadcrumb-icon">{icon}</span>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</Tag>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
BreadcrumbItem.displayName = 'BreadcrumbItem'
|
BreadcrumbItem.displayName = 'BreadcrumbItem'
|
||||||
|
|
||||||
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
|
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>((props, ref) => {
|
||||||
const {
|
const { translate } = useLocalization()
|
||||||
className,
|
const {
|
||||||
children,
|
className,
|
||||||
style,
|
children,
|
||||||
separator = '/',
|
style,
|
||||||
gap = 8,
|
separator = '/',
|
||||||
maxItems,
|
gap = 8,
|
||||||
size = 'md',
|
maxItems,
|
||||||
...rest
|
size = 'md',
|
||||||
} = props
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
const items = Children.toArray(children).filter(isValidElement)
|
const items = Children.toArray(children).filter(isValidElement)
|
||||||
const total = items.length
|
const total = items.length
|
||||||
|
|
||||||
let visibleItems = items
|
let visibleItems = items
|
||||||
let collapsedCount = 0
|
let collapsedCount = 0
|
||||||
|
|
||||||
if (maxItems && total > maxItems) {
|
if (maxItems && total > maxItems) {
|
||||||
collapsedCount = total - maxItems
|
collapsedCount = total - maxItems
|
||||||
visibleItems = [
|
visibleItems = [items[0], ...items.slice(total - (maxItems - 1))]
|
||||||
items[0],
|
}
|
||||||
...items.slice(total - (maxItems - 1)),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedItems = visibleItems.map((item, index) => {
|
const renderedItems = visibleItems.map((item, index) => {
|
||||||
const isLast =
|
const isLast =
|
||||||
maxItems && collapsedCount > 0
|
maxItems && collapsedCount > 0 ? index === visibleItems.length - 1 : index === total - 1
|
||||||
? index === visibleItems.length - 1
|
|
||||||
: index === total - 1
|
|
||||||
|
|
||||||
const cloned = cloneElement(item as React.ReactElement<BreadcrumbItemProps>, {
|
const cloned = cloneElement(item as React.ReactElement<BreadcrumbItemProps>, {
|
||||||
active: isLast,
|
active: isLast,
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={index} className="breadcrumb-entry" style={{ gap }}>
|
|
||||||
{index === 1 && collapsedCount > 0 && (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="breadcrumb-separator"
|
|
||||||
aria-hidden
|
|
||||||
style={{ marginInline: gap }}
|
|
||||||
>
|
|
||||||
{separator}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="breadcrumb-collapsed"
|
|
||||||
title={`${collapsedCount} öğe gizlendi`}
|
|
||||||
>
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{index > 0 && (
|
|
||||||
<span
|
|
||||||
className="breadcrumb-separator"
|
|
||||||
aria-hidden
|
|
||||||
style={{ marginInline: gap }}
|
|
||||||
>
|
|
||||||
{separator}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{cloned}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<span key={index} className="breadcrumb-entry" style={{ gap }}>
|
||||||
ref={ref}
|
{index === 1 && collapsedCount > 0 && (
|
||||||
aria-label="breadcrumb"
|
<>
|
||||||
className={classNames(
|
<span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
|
||||||
'breadcrumb-nav',
|
{separator}
|
||||||
`breadcrumb-${size}`,
|
</span>
|
||||||
className,
|
<span
|
||||||
)}
|
className="breadcrumb-collapsed"
|
||||||
style={style}
|
title={translate('::App.Breadcrumb.CollapsedCount', { count: collapsedCount })}
|
||||||
{...rest}
|
>
|
||||||
>
|
...
|
||||||
<ol className="breadcrumb-list">{renderedItems}</ol>
|
</span>
|
||||||
</nav>
|
</>
|
||||||
|
)}
|
||||||
|
{index > 0 && (
|
||||||
|
<span className="breadcrumb-separator" aria-hidden style={{ marginInline: gap }}>
|
||||||
|
{separator}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cloned}
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
ref={ref}
|
||||||
|
aria-label="breadcrumb"
|
||||||
|
className={classNames('breadcrumb-nav', `breadcrumb-${size}`, className)}
|
||||||
|
style={style}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<ol className="breadcrumb-list">{renderedItems}</ol>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Breadcrumb.displayName = 'Breadcrumb'
|
Breadcrumb.displayName = 'Breadcrumb'
|
||||||
|
|
||||||
const BreadcrumbWithItem = Breadcrumb as typeof Breadcrumb & {
|
const BreadcrumbWithItem = Breadcrumb as typeof Breadcrumb & {
|
||||||
Item: typeof BreadcrumbItem
|
Item: typeof BreadcrumbItem
|
||||||
}
|
}
|
||||||
BreadcrumbWithItem.Item = BreadcrumbItem
|
BreadcrumbWithItem.Item = BreadcrumbItem
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useConfig } from '../ConfigProvider'
|
import { useConfig } from '../ConfigProvider'
|
||||||
|
|
@ -13,240 +13,250 @@ import { useForm } from '../Form/context'
|
||||||
import { useInputGroup } from '../InputGroup/context'
|
import { useInputGroup } from '../InputGroup/context'
|
||||||
import { CONTROL_SIZES } from '../utils/constants'
|
import { CONTROL_SIZES } from '../utils/constants'
|
||||||
import type { CommonProps, TypeAttributes } from '../@types/common'
|
import type { CommonProps, TypeAttributes } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export interface ChipsProps extends CommonProps {
|
export interface ChipsProps extends CommonProps {
|
||||||
/** Kontrollü değer */
|
/** Kontrollü değer */
|
||||||
value?: string[]
|
value?: string[]
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: string[]
|
defaultValue?: string[]
|
||||||
/** Placeholder */
|
/** Placeholder */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Geçersiz */
|
/** Geçersiz */
|
||||||
invalid?: boolean
|
invalid?: boolean
|
||||||
/** Boyut */
|
/** Boyut */
|
||||||
size?: TypeAttributes.ControlSize
|
size?: TypeAttributes.ControlSize
|
||||||
/** Maks chip sayısı. 0 = sınırsız */
|
/** Maks chip sayısı. 0 = sınırsız */
|
||||||
max?: number
|
max?: number
|
||||||
/** Çift değere izin verme. Varsayılan: false */
|
/** Çift değere izin verme. Varsayılan: false */
|
||||||
allowDuplicate?: boolean
|
allowDuplicate?: boolean
|
||||||
/** Enter'a ek ayraç tuşu. Örn: ',' */
|
/** Enter'a ek ayraç tuşu. Örn: ',' */
|
||||||
separator?: string
|
separator?: string
|
||||||
/** Chip özelleştirme render */
|
/** Chip özelleştirme render */
|
||||||
itemTemplate?: (value: string) => ReactNode
|
itemTemplate?: (value: string) => ReactNode
|
||||||
/** Değer değiştiğinde */
|
/** Değer değiştiğinde */
|
||||||
onChange?: (value: string[]) => void
|
onChange?: (value: string[]) => void
|
||||||
/** Chip eklendiğinde */
|
/** Chip eklendiğinde */
|
||||||
onAdd?: (value: string) => void
|
onAdd?: (value: string) => void
|
||||||
/** Chip kaldırıldığında */
|
/** Chip kaldırıldığında */
|
||||||
onRemove?: (value: string) => void
|
onRemove?: (value: string) => void
|
||||||
/** Input adı */
|
/** Input adı */
|
||||||
name?: string
|
name?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
field?: any
|
field?: any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
form?: any
|
form?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
|
const Chips = forwardRef<HTMLDivElement, ChipsProps>((props, ref) => {
|
||||||
const {
|
const { translate } = useLocalization()
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
value: valueProp,
|
||||||
|
defaultValue = [],
|
||||||
|
placeholder,
|
||||||
|
disabled = false,
|
||||||
|
invalid = false,
|
||||||
|
size,
|
||||||
|
max = 0,
|
||||||
|
allowDuplicate = false,
|
||||||
|
separator,
|
||||||
|
itemTemplate,
|
||||||
|
onChange,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
form,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const isControlled = valueProp !== undefined
|
||||||
|
const [chips, setChips] = useState<string[]>(
|
||||||
|
field?.value ?? (isControlled ? valueProp! : defaultValue),
|
||||||
|
)
|
||||||
|
const [inputVal, setInputVal] = useState('')
|
||||||
|
const [focusedChip, setFocusedChip] = useState<number | null>(null)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const { themeColor, primaryColorLevel, controlSize } = useConfig()
|
||||||
|
const formControlSize = useForm()?.size
|
||||||
|
const inputGroupSize = useInputGroup()?.size
|
||||||
|
const resolvedSize = size || inputGroupSize || formControlSize || controlSize
|
||||||
|
const minH = `min-h-${CONTROL_SIZES[resolvedSize]}`
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isControlled) setChips(valueProp!)
|
||||||
|
}, [isControlled, valueProp])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (field?.value !== undefined) setChips(field.value)
|
||||||
|
}, [field?.value])
|
||||||
|
|
||||||
|
const commit = useCallback(
|
||||||
|
(next: string[]) => {
|
||||||
|
if (!isControlled) setChips(next)
|
||||||
|
field?.onChange?.(next)
|
||||||
|
onChange?.(next)
|
||||||
|
},
|
||||||
|
[isControlled, field, onChange],
|
||||||
|
)
|
||||||
|
|
||||||
|
const addChip = useCallback(
|
||||||
|
(raw: string) => {
|
||||||
|
const val = raw.trim()
|
||||||
|
if (!val) return
|
||||||
|
if (!allowDuplicate && chips.includes(val)) return
|
||||||
|
if (max > 0 && chips.length >= max) return
|
||||||
|
const next = [...chips, val]
|
||||||
|
commit(next)
|
||||||
|
onAdd?.(val)
|
||||||
|
setInputVal('')
|
||||||
|
},
|
||||||
|
[chips, allowDuplicate, max, commit, onAdd],
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeChip = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const removed = chips[index]
|
||||||
|
const next = chips.filter((_, i) => i !== index)
|
||||||
|
commit(next)
|
||||||
|
onRemove?.(removed)
|
||||||
|
setFocusedChip(null)
|
||||||
|
inputRef.current?.focus()
|
||||||
|
},
|
||||||
|
[chips, commit, onRemove],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const val = inputVal
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
addChip(val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (separator && e.key === separator) {
|
||||||
|
e.preventDefault()
|
||||||
|
addChip(val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Backspace' && !val && chips.length > 0) {
|
||||||
|
removeChip(chips.length - 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft' && !val) {
|
||||||
|
setFocusedChip(chips.length - 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChipKeyDown = (e: KeyboardEvent<HTMLSpanElement>, index: number) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault()
|
||||||
|
removeChip(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault()
|
||||||
|
setFocusedChip(Math.max(0, index - 1))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (index === chips.length - 1) {
|
||||||
|
setFocusedChip(null)
|
||||||
|
inputRef.current?.focus()
|
||||||
|
} else {
|
||||||
|
setFocusedChip(index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMaxReached = max > 0 && chips.length >= max
|
||||||
|
const ringClass = `focus-within:ring-${themeColor}-${primaryColorLevel}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
'chips',
|
||||||
|
minH,
|
||||||
|
ringClass,
|
||||||
|
invalid && 'chips-invalid',
|
||||||
|
disabled && 'chips-disabled',
|
||||||
className,
|
className,
|
||||||
style,
|
)}
|
||||||
value: valueProp,
|
style={style}
|
||||||
defaultValue = [],
|
onClick={() => inputRef.current?.focus()}
|
||||||
placeholder,
|
{...rest}
|
||||||
disabled = false,
|
>
|
||||||
invalid = false,
|
{chips.map((chip, i) => (
|
||||||
size,
|
<span
|
||||||
max = 0,
|
key={i}
|
||||||
allowDuplicate = false,
|
className="chips-item"
|
||||||
separator,
|
tabIndex={0}
|
||||||
itemTemplate,
|
role="option"
|
||||||
onChange,
|
aria-selected
|
||||||
onAdd,
|
aria-label={chip}
|
||||||
onRemove,
|
onFocus={() => setFocusedChip(i)}
|
||||||
name,
|
onBlur={() => setFocusedChip(null)}
|
||||||
field,
|
onKeyDown={(e) => handleChipKeyDown(e, i)}
|
||||||
form,
|
data-focused={focusedChip === i}
|
||||||
...rest
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const isControlled = valueProp !== undefined
|
|
||||||
const [chips, setChips] = useState<string[]>(
|
|
||||||
field?.value ?? (isControlled ? valueProp! : defaultValue),
|
|
||||||
)
|
|
||||||
const [inputVal, setInputVal] = useState('')
|
|
||||||
const [focusedChip, setFocusedChip] = useState<number | null>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const { themeColor, primaryColorLevel, controlSize } = useConfig()
|
|
||||||
const formControlSize = useForm()?.size
|
|
||||||
const inputGroupSize = useInputGroup()?.size
|
|
||||||
const resolvedSize = size || inputGroupSize || formControlSize || controlSize
|
|
||||||
const minH = `min-h-${CONTROL_SIZES[resolvedSize]}`
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isControlled) setChips(valueProp!)
|
|
||||||
}, [isControlled, valueProp])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (field?.value !== undefined) setChips(field.value)
|
|
||||||
}, [field?.value])
|
|
||||||
|
|
||||||
const commit = useCallback(
|
|
||||||
(next: string[]) => {
|
|
||||||
if (!isControlled) setChips(next)
|
|
||||||
field?.onChange?.(next)
|
|
||||||
onChange?.(next)
|
|
||||||
},
|
|
||||||
[isControlled, field, onChange],
|
|
||||||
)
|
|
||||||
|
|
||||||
const addChip = useCallback(
|
|
||||||
(raw: string) => {
|
|
||||||
const val = raw.trim()
|
|
||||||
if (!val) return
|
|
||||||
if (!allowDuplicate && chips.includes(val)) return
|
|
||||||
if (max > 0 && chips.length >= max) return
|
|
||||||
const next = [...chips, val]
|
|
||||||
commit(next)
|
|
||||||
onAdd?.(val)
|
|
||||||
setInputVal('')
|
|
||||||
},
|
|
||||||
[chips, allowDuplicate, max, commit, onAdd],
|
|
||||||
)
|
|
||||||
|
|
||||||
const removeChip = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const removed = chips[index]
|
|
||||||
const next = chips.filter((_, i) => i !== index)
|
|
||||||
commit(next)
|
|
||||||
onRemove?.(removed)
|
|
||||||
setFocusedChip(null)
|
|
||||||
inputRef.current?.focus()
|
|
||||||
},
|
|
||||||
[chips, commit, onRemove],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
const val = inputVal
|
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
|
||||||
addChip(val)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (separator && e.key === separator) {
|
|
||||||
e.preventDefault()
|
|
||||||
addChip(val)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Backspace' && !val && chips.length > 0) {
|
|
||||||
removeChip(chips.length - 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'ArrowLeft' && !val) {
|
|
||||||
setFocusedChip(chips.length - 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChipKeyDown = (e: KeyboardEvent<HTMLSpanElement>, index: number) => {
|
|
||||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
||||||
e.preventDefault()
|
|
||||||
removeChip(index)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.key === 'ArrowLeft') {
|
|
||||||
e.preventDefault()
|
|
||||||
setFocusedChip(Math.max(0, index - 1))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.key === 'ArrowRight') {
|
|
||||||
e.preventDefault()
|
|
||||||
if (index === chips.length - 1) {
|
|
||||||
setFocusedChip(null)
|
|
||||||
inputRef.current?.focus()
|
|
||||||
} else {
|
|
||||||
setFocusedChip(index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMaxReached = max > 0 && chips.length >= max
|
|
||||||
const ringClass = `focus-within:ring-${themeColor}-${primaryColorLevel}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={classNames(
|
|
||||||
'chips',
|
|
||||||
minH,
|
|
||||||
ringClass,
|
|
||||||
invalid && 'chips-invalid',
|
|
||||||
disabled && 'chips-disabled',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
style={style}
|
|
||||||
onClick={() => inputRef.current?.focus()}
|
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
{chips.map((chip, i) => (
|
<span className="chips-item-label">{itemTemplate ? itemTemplate(chip) : chip}</span>
|
||||||
<span
|
{!disabled && (
|
||||||
key={i}
|
<button
|
||||||
className="chips-item"
|
type="button"
|
||||||
tabIndex={0}
|
className="chips-item-remove"
|
||||||
role="option"
|
onClick={(e) => {
|
||||||
aria-selected
|
e.stopPropagation()
|
||||||
aria-label={chip}
|
removeChip(i)
|
||||||
onFocus={() => setFocusedChip(i)}
|
}}
|
||||||
onBlur={() => setFocusedChip(null)}
|
tabIndex={-1}
|
||||||
onKeyDown={(e) => handleChipKeyDown(e, i)}
|
aria-label={translate('::App.Chips.RemoveChip', { chip })}
|
||||||
data-focused={focusedChip === i}
|
>
|
||||||
>
|
<svg
|
||||||
<span className="chips-item-label">
|
width={10}
|
||||||
{itemTemplate ? itemTemplate(chip) : chip}
|
height={10}
|
||||||
</span>
|
viewBox="0 0 24 24"
|
||||||
{!disabled && (
|
fill="none"
|
||||||
<button
|
stroke="currentColor"
|
||||||
type="button"
|
strokeWidth={2.5}
|
||||||
className="chips-item-remove"
|
>
|
||||||
onClick={(e) => { e.stopPropagation(); removeChip(i) }}
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
tabIndex={-1}
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
aria-label={`${chip} kaldır`}
|
</svg>
|
||||||
>
|
</button>
|
||||||
<svg width={10} height={10} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5}>
|
)}
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
</span>
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
))}
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!isMaxReached && !disabled && (
|
{!isMaxReached && !disabled && (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
name={name ?? field?.name}
|
name={name ?? field?.name}
|
||||||
className="chips-input"
|
className="chips-input"
|
||||||
value={inputVal}
|
value={inputVal}
|
||||||
placeholder={chips.length === 0 ? placeholder : undefined}
|
placeholder={chips.length === 0 ? placeholder : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label="Chip ekle"
|
aria-label={translate('::App.Chips.AddChip')}
|
||||||
onChange={(e) => setInputVal(e.target.value)}
|
onChange={(e) => setInputVal(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (inputVal.trim()) addChip(inputVal)
|
if (inputVal.trim()) addChip(inputVal)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Chips.displayName = 'Chips'
|
Chips.displayName = 'Chips'
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,39 @@
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import type { CommonProps, TypeAttributes } from '../@types/common'
|
import type { CommonProps, TypeAttributes } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
// ── Hex/RGB yardımcı fonksiyonlar ───────────────────────────────────────────
|
// ── Hex/RGB yardımcı fonksiyonlar ───────────────────────────────────────────
|
||||||
|
|
||||||
const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
|
const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
|
||||||
const cleaned = hex.replace('#', '')
|
const cleaned = hex.replace('#', '')
|
||||||
if (cleaned.length !== 6 && cleaned.length !== 3) return null
|
if (cleaned.length !== 6 && cleaned.length !== 3) return null
|
||||||
const full =
|
const full =
|
||||||
cleaned.length === 3
|
cleaned.length === 3
|
||||||
? cleaned
|
? cleaned
|
||||||
.split('')
|
.split('')
|
||||||
.map((c) => c + c)
|
.map((c) => c + c)
|
||||||
.join('')
|
.join('')
|
||||||
: cleaned
|
: cleaned
|
||||||
const num = parseInt(full, 16)
|
const num = parseInt(full, 16)
|
||||||
return {
|
return {
|
||||||
r: (num >> 16) & 255,
|
r: (num >> 16) & 255,
|
||||||
g: (num >> 8) & 255,
|
g: (num >> 8) & 255,
|
||||||
b: num & 255,
|
b: num & 255,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rgbToHex = (r: number, g: number, b: number): string => {
|
const rgbToHex = (r: number, g: number, b: number): string => {
|
||||||
return (
|
return (
|
||||||
'#' +
|
'#' + [r, g, b].map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, '0')).join('')
|
||||||
[r, g, b]
|
)
|
||||||
.map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, '0'))
|
|
||||||
.join('')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val)
|
const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val)
|
||||||
|
|
@ -43,265 +41,253 @@ const isValidHex = (val: string) => /^#[0-9a-fA-F]{6}$/.test(val)
|
||||||
// ── Tipler ──────────────────────────────────────────────────────────────────
|
// ── Tipler ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ColorPickerProps extends CommonProps {
|
export interface ColorPickerProps extends CommonProps {
|
||||||
/** Kontrollü değer (hex: '#rrggbb') */
|
/** Kontrollü değer (hex: '#rrggbb') */
|
||||||
value?: string
|
value?: string
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Boyut */
|
/** Boyut */
|
||||||
size?: TypeAttributes.ControlSize
|
size?: TypeAttributes.ControlSize
|
||||||
/** Önceden tanımlı renk paleti */
|
/** Önceden tanımlı renk paleti */
|
||||||
presets?: string[]
|
presets?: string[]
|
||||||
/** Alpha (opacity) kanalı göster */
|
/** Alpha (opacity) kanalı göster */
|
||||||
showAlpha?: boolean
|
showAlpha?: boolean
|
||||||
/** Hex input göster. Varsayılan: true */
|
/** Hex input göster. Varsayılan: true */
|
||||||
showInput?: boolean
|
showInput?: boolean
|
||||||
/** RGB değerlerini ayrı göster */
|
/** RGB değerlerini ayrı göster */
|
||||||
showRgb?: boolean
|
showRgb?: boolean
|
||||||
/** Değer değiştiğinde callback */
|
/** Değer değiştiğinde callback */
|
||||||
onChange?: (hex: string) => void
|
onChange?: (hex: string) => void
|
||||||
/** Input adı (form entegrasyonu) */
|
/** Input adı (form entegrasyonu) */
|
||||||
name?: string
|
name?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
field?: any
|
field?: any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
form?: any
|
form?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bileşen ─────────────────────────────────────────────────────────────────
|
// ── Bileşen ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
|
const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
|
||||||
const {
|
const { translate } = useLocalization()
|
||||||
className,
|
const {
|
||||||
style,
|
className,
|
||||||
value: valueProp,
|
style,
|
||||||
defaultValue = '#6366f1',
|
value: valueProp,
|
||||||
disabled = false,
|
defaultValue = '#6366f1',
|
||||||
size = 'md',
|
disabled = false,
|
||||||
presets,
|
size = 'md',
|
||||||
showInput = true,
|
presets,
|
||||||
showRgb = false,
|
showInput = true,
|
||||||
onChange,
|
showRgb = false,
|
||||||
name,
|
onChange,
|
||||||
field,
|
name,
|
||||||
form: _form,
|
field,
|
||||||
...rest
|
form: _form,
|
||||||
} = props
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
const isControlled = valueProp !== undefined
|
const isControlled = valueProp !== undefined
|
||||||
const fieldValue = field?.value
|
const fieldValue = field?.value
|
||||||
|
|
||||||
const resolveInitial = () => {
|
const resolveInitial = () => {
|
||||||
if (fieldValue && isValidHex(fieldValue)) return fieldValue
|
if (fieldValue && isValidHex(fieldValue)) return fieldValue
|
||||||
if (isControlled && valueProp && isValidHex(valueProp)) return valueProp!
|
if (isControlled && valueProp && isValidHex(valueProp)) return valueProp!
|
||||||
if (isValidHex(defaultValue)) return defaultValue
|
if (isValidHex(defaultValue)) return defaultValue
|
||||||
return '#6366f1'
|
return '#6366f1'
|
||||||
|
}
|
||||||
|
|
||||||
|
const [internalHex, setInternalHex] = useState<string>(resolveInitial)
|
||||||
|
const [inputText, setInputText] = useState<string>(resolveInitial)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const nativeRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const currentHex = (() => {
|
||||||
|
const src = isControlled ? valueProp! : (fieldValue ?? internalHex)
|
||||||
|
return isValidHex(src) ? src : internalHex
|
||||||
|
})()
|
||||||
|
|
||||||
|
// sync input text ile dış değer
|
||||||
|
useEffect(() => {
|
||||||
|
setInputText(currentHex)
|
||||||
|
}, [currentHex])
|
||||||
|
|
||||||
|
// Dışarı tıklayınca kapat
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
document.addEventListener('mousedown', handler)
|
||||||
|
return () => document.removeEventListener('mousedown', handler)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
const [internalHex, setInternalHex] = useState<string>(resolveInitial)
|
const commit = useCallback(
|
||||||
const [inputText, setInputText] = useState<string>(resolveInitial)
|
(hex: string) => {
|
||||||
const [open, setOpen] = useState(false)
|
if (!isControlled) setInternalHex(hex)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
field?.onChange?.(hex)
|
||||||
const nativeRef = useRef<HTMLInputElement>(null)
|
onChange?.(hex)
|
||||||
|
},
|
||||||
|
[isControlled, onChange, field],
|
||||||
|
)
|
||||||
|
|
||||||
const currentHex = (() => {
|
const handleNativeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const src = isControlled ? valueProp! : fieldValue ?? internalHex
|
const hex = e.target.value
|
||||||
return isValidHex(src) ? src : internalHex
|
setInputText(hex)
|
||||||
})()
|
commit(hex)
|
||||||
|
}
|
||||||
|
|
||||||
// sync input text ile dış değer
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
useEffect(() => {
|
const raw = e.target.value
|
||||||
setInputText(currentHex)
|
setInputText(raw)
|
||||||
}, [currentHex])
|
const normalized = raw.startsWith('#') ? raw : `#${raw}`
|
||||||
|
if (isValidHex(normalized)) {
|
||||||
// Dışarı tıklayınca kapat
|
commit(normalized)
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
containerRef.current &&
|
|
||||||
!containerRef.current.contains(e.target as Node)
|
|
||||||
) {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handler)
|
|
||||||
return () => document.removeEventListener('mousedown', handler)
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const commit = useCallback(
|
|
||||||
(hex: string) => {
|
|
||||||
if (!isControlled) setInternalHex(hex)
|
|
||||||
field?.onChange?.(hex)
|
|
||||||
onChange?.(hex)
|
|
||||||
},
|
|
||||||
[isControlled, onChange, field],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleNativeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const hex = e.target.value
|
|
||||||
setInputText(hex)
|
|
||||||
commit(hex)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputBlur = () => {
|
||||||
const raw = e.target.value
|
const normalized = inputText.startsWith('#') ? inputText : `#${inputText}`
|
||||||
setInputText(raw)
|
if (!isValidHex(normalized)) {
|
||||||
const normalized = raw.startsWith('#') ? raw : `#${raw}`
|
setInputText(currentHex)
|
||||||
if (isValidHex(normalized)) {
|
|
||||||
commit(normalized)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputBlur = () => {
|
const handlePresetClick = (color: string) => {
|
||||||
const normalized = inputText.startsWith('#') ? inputText : `#${inputText}`
|
setInputText(color)
|
||||||
if (!isValidHex(normalized)) {
|
commit(color)
|
||||||
setInputText(currentHex)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePresetClick = (color: string) => {
|
const rgb = hexToRgb(currentHex)
|
||||||
setInputText(color)
|
|
||||||
commit(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rgb = hexToRgb(currentHex)
|
const handleRgbChange = (channel: 'r' | 'g' | 'b', val: string) => {
|
||||||
|
if (!rgb) return
|
||||||
|
const num = Math.min(255, Math.max(0, parseInt(val) || 0))
|
||||||
|
const updated = { ...rgb, [channel]: num }
|
||||||
|
const hex = rgbToHex(updated.r, updated.g, updated.b)
|
||||||
|
setInputText(hex)
|
||||||
|
commit(hex)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRgbChange = (channel: 'r' | 'g' | 'b', val: string) => {
|
const sizeClass = {
|
||||||
if (!rgb) return
|
lg: 'color-picker-lg',
|
||||||
const num = Math.min(255, Math.max(0, parseInt(val) || 0))
|
md: 'color-picker-md',
|
||||||
const updated = { ...rgb, [channel]: num }
|
sm: 'color-picker-sm',
|
||||||
const hex = rgbToHex(updated.r, updated.g, updated.b)
|
xs: 'color-picker-xs',
|
||||||
setInputText(hex)
|
}[size]
|
||||||
commit(hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClass = {
|
return (
|
||||||
lg: 'color-picker-lg',
|
<div
|
||||||
md: 'color-picker-md',
|
ref={(node) => {
|
||||||
sm: 'color-picker-sm',
|
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||||
xs: 'color-picker-xs',
|
if (typeof ref === 'function') ref(node)
|
||||||
}[size]
|
else if (ref) ref.current = node
|
||||||
|
}}
|
||||||
|
className={classNames('color-picker', sizeClass, className)}
|
||||||
|
style={style}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{/* Hidden native color input (asıl değer kaynağı) */}
|
||||||
|
<input
|
||||||
|
ref={nativeRef}
|
||||||
|
type="color"
|
||||||
|
name={name ?? field?.name}
|
||||||
|
value={currentHex}
|
||||||
|
disabled={disabled}
|
||||||
|
className="color-picker-native"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
|
onChange={handleNativeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
{/* Tetikleyici swatch */}
|
||||||
<div
|
<button
|
||||||
ref={(node) => {
|
type="button"
|
||||||
;(
|
className={classNames('color-picker-swatch', disabled && 'color-picker-disabled')}
|
||||||
containerRef as React.MutableRefObject<HTMLDivElement | null>
|
style={{ backgroundColor: currentHex }}
|
||||||
).current = node
|
disabled={disabled}
|
||||||
if (typeof ref === 'function') ref(node)
|
aria-label={translate('::App.ColorPicker.PickColor')}
|
||||||
else if (ref) ref.current = node
|
onClick={() => {
|
||||||
}}
|
if (disabled) return
|
||||||
className={classNames('color-picker', sizeClass, className)}
|
setOpen((o) => !o)
|
||||||
style={style}
|
}}
|
||||||
{...rest}
|
/>
|
||||||
>
|
|
||||||
{/* Hidden native color input (asıl değer kaynağı) */}
|
{/* Popup panel */}
|
||||||
|
{open && (
|
||||||
|
<div className="color-picker-panel">
|
||||||
|
{/* Native picker (geniş spektrum) */}
|
||||||
|
<div className="color-picker-spectrum-wrapper">
|
||||||
<input
|
<input
|
||||||
ref={nativeRef}
|
type="color"
|
||||||
type="color"
|
value={currentHex}
|
||||||
name={name ?? field?.name}
|
className="color-picker-spectrum"
|
||||||
value={currentHex}
|
onChange={handleNativeChange}
|
||||||
disabled={disabled}
|
|
||||||
className="color-picker-native"
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-hidden
|
|
||||||
onChange={handleNativeChange}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Tetikleyici swatch */}
|
{/* Preset renkler */}
|
||||||
<button
|
{presets && presets.length > 0 && (
|
||||||
type="button"
|
<div className="color-picker-presets">
|
||||||
className={classNames(
|
{presets.map((color) => (
|
||||||
'color-picker-swatch',
|
<button
|
||||||
disabled && 'color-picker-disabled',
|
key={color}
|
||||||
)}
|
type="button"
|
||||||
style={{ backgroundColor: currentHex }}
|
className={classNames(
|
||||||
disabled={disabled}
|
'color-picker-preset-dot',
|
||||||
aria-label="Renk seç"
|
currentHex.toLowerCase() === color.toLowerCase() &&
|
||||||
onClick={() => {
|
'color-picker-preset-dot-active',
|
||||||
if (disabled) return
|
)}
|
||||||
setOpen((o) => !o)
|
style={{ backgroundColor: color }}
|
||||||
}}
|
title={color}
|
||||||
/>
|
onClick={() => handlePresetClick(color)}
|
||||||
|
aria-label={color}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Popup panel */}
|
{/* Hex input */}
|
||||||
{open && (
|
{showInput && (
|
||||||
<div className="color-picker-panel">
|
<div className="color-picker-input-row">
|
||||||
{/* Native picker (geniş spektrum) */}
|
<span className="color-picker-input-label">HEX</span>
|
||||||
<div className="color-picker-spectrum-wrapper">
|
<input
|
||||||
<input
|
className="color-picker-text-input"
|
||||||
type="color"
|
value={inputText}
|
||||||
value={currentHex}
|
maxLength={7}
|
||||||
className="color-picker-spectrum"
|
onChange={handleInputChange}
|
||||||
onChange={handleNativeChange}
|
onBlur={handleInputBlur}
|
||||||
/>
|
spellCheck={false}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Preset renkler */}
|
{/* RGB inputs */}
|
||||||
{presets && presets.length > 0 && (
|
{showRgb && rgb && (
|
||||||
<div className="color-picker-presets">
|
<div className="color-picker-rgb-row">
|
||||||
{presets.map((color) => (
|
{(['r', 'g', 'b'] as const).map((ch) => (
|
||||||
<button
|
<div key={ch} className="color-picker-rgb-field">
|
||||||
key={color}
|
<input
|
||||||
type="button"
|
className="color-picker-text-input"
|
||||||
className={classNames(
|
type="number"
|
||||||
'color-picker-preset-dot',
|
min={0}
|
||||||
currentHex.toLowerCase() ===
|
max={255}
|
||||||
color.toLowerCase() &&
|
value={rgb[ch]}
|
||||||
'color-picker-preset-dot-active',
|
onChange={(e) => handleRgbChange(ch, e.target.value)}
|
||||||
)}
|
/>
|
||||||
style={{ backgroundColor: color }}
|
<span className="color-picker-input-label">{ch.toUpperCase()}</span>
|
||||||
title={color}
|
|
||||||
onClick={() => handlePresetClick(color)}
|
|
||||||
aria-label={color}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hex input */}
|
|
||||||
{showInput && (
|
|
||||||
<div className="color-picker-input-row">
|
|
||||||
<span className="color-picker-input-label">HEX</span>
|
|
||||||
<input
|
|
||||||
className="color-picker-text-input"
|
|
||||||
value={inputText}
|
|
||||||
maxLength={7}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onBlur={handleInputBlur}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* RGB inputs */}
|
|
||||||
{showRgb && rgb && (
|
|
||||||
<div className="color-picker-rgb-row">
|
|
||||||
{(['r', 'g', 'b'] as const).map((ch) => (
|
|
||||||
<div key={ch} className="color-picker-rgb-field">
|
|
||||||
<input
|
|
||||||
className="color-picker-text-input"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={255}
|
|
||||||
value={rgb[ch]}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleRgbChange(ch, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="color-picker-input-label">
|
|
||||||
{ch.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
ColorPicker.displayName = 'ColorPicker'
|
ColorPicker.displayName = 'ColorPicker'
|
||||||
|
|
|
||||||
|
|
@ -1,500 +1,523 @@
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type MouseEvent,
|
type MouseEvent,
|
||||||
type WheelEvent,
|
type WheelEvent,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import type { CommonProps } from '../@types/common'
|
import type { CommonProps } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
// ── Tipler ──────────────────────────────────────────────────────────────────
|
// ── Tipler ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ImageViewerImage {
|
export interface ImageViewerImage {
|
||||||
src: string
|
src: string
|
||||||
alt?: string
|
alt?: string
|
||||||
caption?: string
|
caption?: string
|
||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageViewerProps extends CommonProps {
|
export interface ImageViewerProps extends CommonProps {
|
||||||
/** Görüntülenecek resimler */
|
/** Görüntülenecek resimler */
|
||||||
images: ImageViewerImage[]
|
images: ImageViewerImage[]
|
||||||
/** Açılış resmi index'i (kontrolsüz) */
|
/** Açılış resmi index'i (kontrolsüz) */
|
||||||
defaultIndex?: number
|
defaultIndex?: number
|
||||||
/** Kontrollü açık/kapalı durumu */
|
/** Kontrollü açık/kapalı durumu */
|
||||||
open?: boolean
|
open?: boolean
|
||||||
/** Kontrollü aktif index */
|
/** Kontrollü aktif index */
|
||||||
activeIndex?: number
|
activeIndex?: number
|
||||||
/** Kapatma isteği */
|
/** Kapatma isteği */
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
/** Index değişimi */
|
/** Index değişimi */
|
||||||
onIndexChange?: (index: number) => void
|
onIndexChange?: (index: number) => void
|
||||||
/** Döngü. Varsayılan: true */
|
/** Döngü. Varsayılan: true */
|
||||||
loop?: boolean
|
loop?: boolean
|
||||||
/** Toolbar'ı göster. Varsayılan: true */
|
/** Toolbar'ı göster. Varsayılan: true */
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
/** Thumbnailleri göster. Varsayılan: true */
|
/** Thumbnailleri göster. Varsayılan: true */
|
||||||
showThumbnails?: boolean
|
showThumbnails?: boolean
|
||||||
/** Özel toolbar başlık sağ alanı */
|
/** Özel toolbar başlık sağ alanı */
|
||||||
toolbarExtra?: ReactNode
|
toolbarExtra?: ReactNode
|
||||||
/** Klavye navigasyonu. Varsayılan: true */
|
/** Klavye navigasyonu. Varsayılan: true */
|
||||||
keyboard?: boolean
|
keyboard?: boolean
|
||||||
/** Zoom adımı. Varsayılan: 0.25 */
|
/** Zoom adımı. Varsayılan: 0.25 */
|
||||||
zoomStep?: number
|
zoomStep?: number
|
||||||
/** Min zoom. Varsayılan: 0.5 */
|
/** Min zoom. Varsayılan: 0.5 */
|
||||||
minZoom?: number
|
minZoom?: number
|
||||||
/** Max zoom. Varsayılan: 4 */
|
/** Max zoom. Varsayılan: 4 */
|
||||||
maxZoom?: number
|
maxZoom?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── İkonlar ──────────────────────────────────────────────────────────────────
|
// ── İkonlar ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const IconClose = () => (
|
const IconClose = () => (
|
||||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconPrev = () => (
|
const IconPrev = () => (
|
||||||
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<polyline points="15 18 9 12 15 6" />
|
<polyline points="15 18 9 12 15 6" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconNext = () => (
|
const IconNext = () => (
|
||||||
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<polyline points="9 18 15 12 9 6" />
|
<polyline points="9 18 15 12 9 6" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconZoomIn = () => (
|
const IconZoomIn = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<circle cx="11" cy="11" r="8" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
<line x1="11" y1="8" x2="11" y2="14" />
|
<line x1="11" y1="8" x2="11" y2="14" />
|
||||||
<line x1="8" y1="11" x2="14" y2="11" />
|
<line x1="8" y1="11" x2="14" y2="11" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconZoomOut = () => (
|
const IconZoomOut = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<circle cx="11" cy="11" r="8" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
<line x1="8" y1="11" x2="14" y2="11" />
|
<line x1="8" y1="11" x2="14" y2="11" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconReset = () => (
|
const IconReset = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<polyline points="1 4 1 10 7 10" />
|
<polyline points="1 4 1 10 7 10" />
|
||||||
<path d="M3.51 15a9 9 0 1 0 .49-4.95" />
|
<path d="M3.51 15a9 9 0 1 0 .49-4.95" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconRotateCW = () => (
|
const IconRotateCW = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<polyline points="23 4 23 10 17 10" />
|
<polyline points="23 4 23 10 17 10" />
|
||||||
<path d="M20.49 15a9 9 0 1 1-.49-4.95" />
|
<path d="M20.49 15a9 9 0 1 1-.49-4.95" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconRotateCCW = () => (
|
const IconRotateCCW = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<polyline points="1 4 1 10 7 10" />
|
<polyline points="1 4 1 10 7 10" />
|
||||||
<path d="M3.51 15a9 9 0 1 0 .49-4.95" />
|
<path d="M3.51 15a9 9 0 1 0 .49-4.95" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
const IconDownload = () => (
|
const IconDownload = () => (
|
||||||
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
<polyline points="7 10 12 15 17 10" />
|
<polyline points="7 10 12 15 17 10" />
|
||||||
<line x1="12" y1="15" x2="12" y2="3" />
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Overlay ──────────────────────────────────────────────────────────────────
|
// ── Overlay ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ImageViewerOverlay = ({
|
const ImageViewerOverlay = ({
|
||||||
images,
|
images,
|
||||||
index,
|
index,
|
||||||
loop,
|
loop,
|
||||||
showToolbar,
|
showToolbar,
|
||||||
showThumbnails,
|
showThumbnails,
|
||||||
toolbarExtra,
|
toolbarExtra,
|
||||||
zoomStep,
|
zoomStep,
|
||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
onClose,
|
onClose,
|
||||||
onIndexChange,
|
onIndexChange,
|
||||||
}: {
|
}: {
|
||||||
images: ImageViewerImage[]
|
images: ImageViewerImage[]
|
||||||
index: number
|
index: number
|
||||||
loop: boolean
|
loop: boolean
|
||||||
showToolbar: boolean
|
showToolbar: boolean
|
||||||
showThumbnails: boolean
|
showThumbnails: boolean
|
||||||
toolbarExtra?: ReactNode
|
toolbarExtra?: ReactNode
|
||||||
zoomStep: number
|
zoomStep: number
|
||||||
minZoom: number
|
minZoom: number
|
||||||
maxZoom: number
|
maxZoom: number
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onIndexChange: (i: number) => void
|
onIndexChange: (i: number) => void
|
||||||
}) => {
|
}) => {
|
||||||
const [zoom, setZoom] = useState(1)
|
const { translate } = useLocalization()
|
||||||
const [rotation, setRotation] = useState(0)
|
const [zoom, setZoom] = useState(1)
|
||||||
const [dragging, setDragging] = useState(false)
|
const [rotation, setRotation] = useState(0)
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 })
|
const [dragging, setDragging] = useState(false)
|
||||||
const dragStart = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null)
|
const [offset, setOffset] = useState({ x: 0, y: 0 })
|
||||||
const imgRef = useRef<HTMLImageElement>(null)
|
const dragStart = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null)
|
||||||
|
const imgRef = useRef<HTMLImageElement>(null)
|
||||||
|
|
||||||
const current = images[index]
|
const current = images[index]
|
||||||
const hasPrev = loop ? images.length > 1 : index > 0
|
const hasPrev = loop ? images.length > 1 : index > 0
|
||||||
const hasNext = loop ? images.length > 1 : index < images.length - 1
|
const hasNext = loop ? images.length > 1 : index < images.length - 1
|
||||||
|
|
||||||
const resetTransform = useCallback(() => {
|
const resetTransform = useCallback(() => {
|
||||||
setZoom(1)
|
setZoom(1)
|
||||||
setRotation(0)
|
setRotation(0)
|
||||||
setOffset({ x: 0, y: 0 })
|
setOffset({ x: 0, y: 0 })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Reset on image change
|
// Reset on image change
|
||||||
useEffect(() => { resetTransform() }, [index, resetTransform])
|
useEffect(() => {
|
||||||
|
resetTransform()
|
||||||
|
}, [index, resetTransform])
|
||||||
|
|
||||||
const goNext = useCallback(() => {
|
const goNext = useCallback(() => {
|
||||||
if (!hasNext) return
|
if (!hasNext) return
|
||||||
onIndexChange(loop ? (index + 1) % images.length : index + 1)
|
onIndexChange(loop ? (index + 1) % images.length : index + 1)
|
||||||
}, [hasNext, loop, index, images.length, onIndexChange])
|
}, [hasNext, loop, index, images.length, onIndexChange])
|
||||||
|
|
||||||
const goPrev = useCallback(() => {
|
const goPrev = useCallback(() => {
|
||||||
if (!hasPrev) return
|
if (!hasPrev) return
|
||||||
onIndexChange(loop ? (index - 1 + images.length) % images.length : index - 1)
|
onIndexChange(loop ? (index - 1 + images.length) % images.length : index - 1)
|
||||||
}, [hasPrev, loop, index, images.length, onIndexChange])
|
}, [hasPrev, loop, index, images.length, onIndexChange])
|
||||||
|
|
||||||
const zoomIn = useCallback(() =>
|
const zoomIn = useCallback(
|
||||||
setZoom((z) => Math.min(z + zoomStep, maxZoom)), [zoomStep, maxZoom])
|
() => setZoom((z) => Math.min(z + zoomStep, maxZoom)),
|
||||||
const zoomOut = useCallback(() =>
|
[zoomStep, maxZoom],
|
||||||
setZoom((z) => Math.max(z - zoomStep, minZoom)), [zoomStep, minZoom])
|
)
|
||||||
const rotateCW = useCallback(() => setRotation((r) => r + 90), [])
|
const zoomOut = useCallback(
|
||||||
const rotateCCW = useCallback(() => setRotation((r) => r - 90), [])
|
() => setZoom((z) => Math.max(z - zoomStep, minZoom)),
|
||||||
|
[zoomStep, minZoom],
|
||||||
|
)
|
||||||
|
const rotateCW = useCallback(() => setRotation((r) => r + 90), [])
|
||||||
|
const rotateCCW = useCallback(() => setRotation((r) => r - 90), [])
|
||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
const src = current.src
|
const src = current.src
|
||||||
const isDataUri = src.startsWith('data:')
|
const isDataUri = src.startsWith('data:')
|
||||||
|
|
||||||
if (isDataUri) {
|
if (isDataUri) {
|
||||||
// data:[<mime>];base64,<data> veya data:[<mime>],<data>
|
// data:[<mime>];base64,<data> veya data:[<mime>],<data>
|
||||||
const mimeMatch = src.match(/^data:([^;,]+)/)
|
const mimeMatch = src.match(/^data:([^;,]+)/)
|
||||||
const mime = mimeMatch?.[1] ?? 'image/png'
|
const mime = mimeMatch?.[1] ?? 'image/png'
|
||||||
const ext = mime.split('/')[1]?.replace('jpeg', 'jpg') ?? 'png'
|
const ext = mime.split('/')[1]?.replace('jpeg', 'jpg') ?? 'png'
|
||||||
const baseName = current.alt
|
const baseName = current.alt ? current.alt.replace(/\.[^.]+$/, '') : 'image'
|
||||||
? current.alt.replace(/\.[^.]+$/, '')
|
const a = document.createElement('a')
|
||||||
: 'image'
|
a.href = src
|
||||||
const a = document.createElement('a')
|
a.download = `${baseName}.${ext}`
|
||||||
a.href = src
|
a.click()
|
||||||
a.download = `${baseName}.${ext}`
|
} else {
|
||||||
a.click()
|
// Normal URL - dosya adını URL'den çıkar, yoksa alt veya 'image' kullan
|
||||||
} else {
|
let fileName = current.alt ?? ''
|
||||||
// Normal URL - dosya adını URL'den çıkar, yoksa alt veya 'image' kullan
|
if (!fileName) {
|
||||||
let fileName = current.alt ?? ''
|
try {
|
||||||
if (!fileName) {
|
const url = new URL(src, window.location.href)
|
||||||
try {
|
const pathParts = url.pathname.split('/')
|
||||||
const url = new URL(src, window.location.href)
|
fileName = pathParts[pathParts.length - 1] || 'image'
|
||||||
const pathParts = url.pathname.split('/')
|
} catch {
|
||||||
fileName = pathParts[pathParts.length - 1] || 'image'
|
fileName = 'image'
|
||||||
} catch {
|
|
||||||
fileName = 'image'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = src
|
|
||||||
a.download = fileName
|
|
||||||
// Aynı origin değilse fetch + blob ile zorla indir
|
|
||||||
try {
|
|
||||||
const isSameOrigin =
|
|
||||||
new URL(src, window.location.href).origin === window.location.origin
|
|
||||||
if (!isSameOrigin) {
|
|
||||||
fetch(src)
|
|
||||||
.then((r) => r.blob())
|
|
||||||
.then((blob) => {
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
|
||||||
a.href = blobUrl
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(blobUrl)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch { /* origin parse hatası — direkt dene */ }
|
|
||||||
a.click()
|
|
||||||
}
|
}
|
||||||
}, [current])
|
}
|
||||||
|
const a = document.createElement('a')
|
||||||
// Keyboard
|
a.href = src
|
||||||
useEffect(() => {
|
a.download = fileName
|
||||||
const handler = (e: KeyboardEvent) => {
|
// Aynı origin değilse fetch + blob ile zorla indir
|
||||||
switch (e.key) {
|
try {
|
||||||
case 'ArrowLeft': goPrev(); break
|
const isSameOrigin = new URL(src, window.location.href).origin === window.location.origin
|
||||||
case 'ArrowRight': goNext(); break
|
if (!isSameOrigin) {
|
||||||
case 'Escape': onClose(); break
|
fetch(src)
|
||||||
case '+': case '=': zoomIn(); break
|
.then((r) => r.blob())
|
||||||
case '-': zoomOut(); break
|
.then((blob) => {
|
||||||
case 'r': rotateCW(); break
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
case 'R': rotateCCW(); break
|
a.href = blobUrl
|
||||||
}
|
a.click()
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', handler)
|
} catch {
|
||||||
return () => window.removeEventListener('keydown', handler)
|
/* origin parse hatası — direkt dene */
|
||||||
}, [goPrev, goNext, onClose, zoomIn, zoomOut, rotateCW, rotateCCW])
|
}
|
||||||
|
a.click()
|
||||||
// Wheel zoom
|
|
||||||
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.deltaY < 0) zoomIn()
|
|
||||||
else zoomOut()
|
|
||||||
}
|
}
|
||||||
|
}, [current])
|
||||||
|
|
||||||
// Drag (pan)
|
// Keyboard
|
||||||
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
useEffect(() => {
|
||||||
if (zoom <= 1) return
|
const handler = (e: KeyboardEvent) => {
|
||||||
e.preventDefault()
|
switch (e.key) {
|
||||||
setDragging(true)
|
case 'ArrowLeft':
|
||||||
dragStart.current = { x: e.clientX, y: e.clientY, ox: offset.x, oy: offset.y }
|
goPrev()
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
goNext()
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
onClose()
|
||||||
|
break
|
||||||
|
case '+':
|
||||||
|
case '=':
|
||||||
|
zoomIn()
|
||||||
|
break
|
||||||
|
case '-':
|
||||||
|
zoomOut()
|
||||||
|
break
|
||||||
|
case 'r':
|
||||||
|
rotateCW()
|
||||||
|
break
|
||||||
|
case 'R':
|
||||||
|
rotateCCW()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
window.addEventListener('keydown', handler)
|
||||||
if (!dragging || !dragStart.current) return
|
return () => window.removeEventListener('keydown', handler)
|
||||||
setOffset({
|
}, [goPrev, goNext, onClose, zoomIn, zoomOut, rotateCW, rotateCCW])
|
||||||
x: dragStart.current.ox + (e.clientX - dragStart.current.x),
|
|
||||||
y: dragStart.current.oy + (e.clientY - dragStart.current.y),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const handleMouseUp = () => { setDragging(false); dragStart.current = null }
|
|
||||||
|
|
||||||
const overlayClick = (e: MouseEvent<HTMLDivElement>) => {
|
// Wheel zoom
|
||||||
if (e.target === e.currentTarget) onClose()
|
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
|
||||||
}
|
e.preventDefault()
|
||||||
|
if (e.deltaY < 0) zoomIn()
|
||||||
|
else zoomOut()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
// Drag (pan)
|
||||||
<div
|
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
className="image-viewer-overlay"
|
if (zoom <= 1) return
|
||||||
role="dialog"
|
e.preventDefault()
|
||||||
aria-modal
|
setDragging(true)
|
||||||
aria-label={current.alt ?? 'Resim görüntüleyici'}
|
dragStart.current = { x: e.clientX, y: e.clientY, ox: offset.x, oy: offset.y }
|
||||||
>
|
}
|
||||||
{/* Toolbar */}
|
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
{showToolbar && (
|
if (!dragging || !dragStart.current) return
|
||||||
<div className="image-viewer-toolbar">
|
setOffset({
|
||||||
<span className="image-viewer-counter">
|
x: dragStart.current.ox + (e.clientX - dragStart.current.x),
|
||||||
{index + 1} / {images.length}
|
y: dragStart.current.oy + (e.clientY - dragStart.current.y),
|
||||||
</span>
|
})
|
||||||
{current.caption && (
|
}
|
||||||
<span className="image-viewer-caption">{current.caption}</span>
|
const handleMouseUp = () => {
|
||||||
)}
|
setDragging(false)
|
||||||
<div className="image-viewer-toolbar-actions">
|
dragStart.current = null
|
||||||
{toolbarExtra}
|
}
|
||||||
<button type="button" onClick={zoomOut} title="Uzaklaştır (-)">
|
|
||||||
<IconZoomOut />
|
|
||||||
</button>
|
|
||||||
<span className="image-viewer-zoom-label">
|
|
||||||
{Math.round(zoom * 100)}%
|
|
||||||
</span>
|
|
||||||
<button type="button" onClick={zoomIn} title="Yaklaştır (+)">
|
|
||||||
<IconZoomIn />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={resetTransform} title="Sıfırla">
|
|
||||||
<IconReset />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={rotateCCW} title="Sola döndür (R)">
|
|
||||||
<IconRotateCCW />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={rotateCW} title="Sağa döndür (r)">
|
|
||||||
<IconRotateCW />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={handleDownload} title="İndir">
|
|
||||||
<IconDownload />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={onClose} title="Kapat (Esc)" className="image-viewer-close">
|
|
||||||
<IconClose />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stage */}
|
const overlayClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
<div
|
if (e.target === e.currentTarget) onClose()
|
||||||
className={classNames(
|
}
|
||||||
'image-viewer-stage',
|
|
||||||
dragging && 'image-viewer-dragging',
|
return (
|
||||||
)}
|
<div
|
||||||
onClick={overlayClick}
|
className="image-viewer-overlay"
|
||||||
onWheel={handleWheel}
|
role="dialog"
|
||||||
onMouseDown={handleMouseDown}
|
aria-modal
|
||||||
onMouseMove={handleMouseMove}
|
aria-label={current.alt ?? translate('::App.ImageViewer.Title')}
|
||||||
onMouseUp={handleMouseUp}
|
>
|
||||||
onMouseLeave={handleMouseUp}
|
{/* Toolbar */}
|
||||||
|
{showToolbar && (
|
||||||
|
<div className="image-viewer-toolbar">
|
||||||
|
<span className="image-viewer-counter">
|
||||||
|
{index + 1} / {images.length}
|
||||||
|
</span>
|
||||||
|
{current.caption && <span className="image-viewer-caption">{current.caption}</span>}
|
||||||
|
<div className="image-viewer-toolbar-actions">
|
||||||
|
{toolbarExtra}
|
||||||
|
<button type="button" onClick={zoomOut} title={translate('::App.Platform.ZoomOut')}>
|
||||||
|
<IconZoomOut />
|
||||||
|
</button>
|
||||||
|
<span className="image-viewer-zoom-label">{Math.round(zoom * 100)}%</span>
|
||||||
|
<button type="button" onClick={zoomIn} title={translate('::App.Platform.ZoomIn')}>
|
||||||
|
<IconZoomIn />
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={resetTransform} title={translate('::Reset')}>
|
||||||
|
<IconReset />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={rotateCCW}
|
||||||
|
title={translate('::App.ImageViewer.RotateLeft')}
|
||||||
>
|
>
|
||||||
<img
|
<IconRotateCCW />
|
||||||
ref={imgRef}
|
</button>
|
||||||
src={current.src}
|
<button
|
||||||
alt={current.alt ?? ''}
|
type="button"
|
||||||
className="image-viewer-img"
|
onClick={rotateCW}
|
||||||
draggable={false}
|
title={translate('::App.ImageViewer.RotateRight')}
|
||||||
style={{
|
>
|
||||||
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
|
<IconRotateCW />
|
||||||
cursor: zoom > 1 ? (dragging ? 'grabbing' : 'grab') : 'default',
|
</button>
|
||||||
}}
|
<button type="button" onClick={handleDownload} title={translate('::Download')}>
|
||||||
/>
|
<IconDownload />
|
||||||
</div>
|
</button>
|
||||||
|
<button
|
||||||
{/* Prev / Next */}
|
type="button"
|
||||||
{hasPrev && (
|
onClick={onClose}
|
||||||
<button
|
title={translate('::App.Platform.CloseWithEsc')}
|
||||||
type="button"
|
className="image-viewer-close"
|
||||||
className="image-viewer-nav image-viewer-nav-prev"
|
>
|
||||||
onClick={goPrev}
|
<IconClose />
|
||||||
aria-label="Önceki"
|
</button>
|
||||||
>
|
</div>
|
||||||
<IconPrev />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{hasNext && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="image-viewer-nav image-viewer-nav-next"
|
|
||||||
onClick={goNext}
|
|
||||||
aria-label="Sonraki"
|
|
||||||
>
|
|
||||||
<IconNext />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Thumbnails */}
|
|
||||||
{showThumbnails && images.length > 1 && (
|
|
||||||
<div className="image-viewer-thumbnails">
|
|
||||||
{images.map((img, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onIndexChange(i)}
|
|
||||||
className={classNames(
|
|
||||||
'image-viewer-thumb',
|
|
||||||
i === index && 'image-viewer-thumb-active',
|
|
||||||
)}
|
|
||||||
aria-label={img.alt ?? `Resim ${i + 1}`}
|
|
||||||
aria-pressed={i === index}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={img.thumbnail ?? img.src}
|
|
||||||
alt={img.alt ?? ''}
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
|
|
||||||
|
{/* Stage */}
|
||||||
|
<div
|
||||||
|
className={classNames('image-viewer-stage', dragging && 'image-viewer-dragging')}
|
||||||
|
onClick={overlayClick}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseUp}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
src={current.src}
|
||||||
|
alt={current.alt ?? ''}
|
||||||
|
className="image-viewer-img"
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
|
||||||
|
cursor: zoom > 1 ? (dragging ? 'grabbing' : 'grab') : 'default',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prev / Next */}
|
||||||
|
{hasPrev && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-viewer-nav image-viewer-nav-prev"
|
||||||
|
onClick={goPrev}
|
||||||
|
aria-label={translate('::App.VisualDesigner.Canvas.Previous')}
|
||||||
|
>
|
||||||
|
<IconPrev />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{hasNext && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-viewer-nav image-viewer-nav-next"
|
||||||
|
onClick={goNext}
|
||||||
|
aria-label={translate('::App.VisualDesigner.Canvas.Next')}
|
||||||
|
>
|
||||||
|
<IconNext />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Thumbnails */}
|
||||||
|
{showThumbnails && images.length > 1 && (
|
||||||
|
<div className="image-viewer-thumbnails">
|
||||||
|
{images.map((img, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onIndexChange(i)}
|
||||||
|
className={classNames(
|
||||||
|
'image-viewer-thumb',
|
||||||
|
i === index && 'image-viewer-thumb-active',
|
||||||
|
)}
|
||||||
|
aria-label={img.alt ?? translate('::App.ImageViewer.ImageN', { index: i + 1 })}
|
||||||
|
aria-pressed={i === index}
|
||||||
|
>
|
||||||
|
<img src={img.thumbnail ?? img.src} alt={img.alt ?? ''} draggable={false} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ana Komponent ────────────────────────────────────────────────────────────
|
// ── Ana Komponent ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => {
|
const ImageViewer = forwardRef<HTMLDivElement, ImageViewerProps>((props, ref) => {
|
||||||
const {
|
const { translate } = useLocalization()
|
||||||
className,
|
const {
|
||||||
style,
|
className,
|
||||||
images,
|
style,
|
||||||
defaultIndex = 0,
|
images,
|
||||||
open: openProp,
|
defaultIndex = 0,
|
||||||
activeIndex: activeIndexProp,
|
open: openProp,
|
||||||
onClose,
|
activeIndex: activeIndexProp,
|
||||||
onIndexChange,
|
onClose,
|
||||||
loop = true,
|
onIndexChange,
|
||||||
showToolbar = true,
|
loop = true,
|
||||||
showThumbnails = true,
|
showToolbar = true,
|
||||||
toolbarExtra,
|
showThumbnails = true,
|
||||||
keyboard: _keyboard = true,
|
toolbarExtra,
|
||||||
zoomStep = 0.25,
|
keyboard: _keyboard = true,
|
||||||
minZoom = 0.5,
|
zoomStep = 0.25,
|
||||||
maxZoom = 4,
|
minZoom = 0.5,
|
||||||
children,
|
maxZoom = 4,
|
||||||
...rest
|
children,
|
||||||
} = props
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
const isControlled = openProp !== undefined
|
const isControlled = openProp !== undefined
|
||||||
const [internalOpen, setInternalOpen] = useState(false)
|
const [internalOpen, setInternalOpen] = useState(false)
|
||||||
const [internalIndex, setInternalIndex] = useState(defaultIndex)
|
const [internalIndex, setInternalIndex] = useState(defaultIndex)
|
||||||
|
|
||||||
const isOpen = isControlled ? openProp! : internalOpen
|
const isOpen = isControlled ? openProp! : internalOpen
|
||||||
const currentIndex =
|
const currentIndex = activeIndexProp !== undefined ? activeIndexProp : internalIndex
|
||||||
activeIndexProp !== undefined ? activeIndexProp : internalIndex
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
if (!isControlled) setInternalOpen(false)
|
if (!isControlled) setInternalOpen(false)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
}, [isControlled, onClose])
|
}, [isControlled, onClose])
|
||||||
|
|
||||||
const handleIndexChange = useCallback(
|
const handleIndexChange = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
if (activeIndexProp === undefined) setInternalIndex(i)
|
if (activeIndexProp === undefined) setInternalIndex(i)
|
||||||
onIndexChange?.(i)
|
onIndexChange?.(i)
|
||||||
},
|
},
|
||||||
[activeIndexProp, onIndexChange],
|
[activeIndexProp, onIndexChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
const openAt = useCallback(
|
const openAt = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setInternalIndex(index)
|
setInternalIndex(index)
|
||||||
if (!isControlled) setInternalOpen(true)
|
if (!isControlled) setInternalOpen(true)
|
||||||
},
|
},
|
||||||
[isControlled],
|
[isControlled],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!images || images.length === 0) return null
|
if (!images || images.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Trigger wrapper (children varsa tıklanabilir galeri) */}
|
{/* Trigger wrapper (children varsa tıklanabilir galeri) */}
|
||||||
{children && (
|
{children && (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames('image-viewer-trigger', className)}
|
className={classNames('image-viewer-trigger', className)}
|
||||||
style={style}
|
style={style}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{images.map((img, i) => (
|
{images.map((img, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
type="button"
|
type="button"
|
||||||
className="image-viewer-trigger-item"
|
className="image-viewer-trigger-item"
|
||||||
onClick={() => openAt(i)}
|
onClick={() => openAt(i)}
|
||||||
aria-label={img.alt ?? `Resim ${i + 1}`}
|
aria-label={img.alt ?? translate('::App.ImageViewer.ImageN', { index: i + 1 })}
|
||||||
>
|
>
|
||||||
<img
|
<img src={img.thumbnail ?? img.src} alt={img.alt ?? ''} draggable={false} />
|
||||||
src={img.thumbnail ?? img.src}
|
</button>
|
||||||
alt={img.alt ?? ''}
|
))}
|
||||||
draggable={false}
|
</div>
|
||||||
/>
|
)}
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Portal overlay */}
|
{/* Portal overlay */}
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<ImageViewerOverlay
|
<ImageViewerOverlay
|
||||||
images={images}
|
images={images}
|
||||||
index={currentIndex}
|
index={currentIndex}
|
||||||
loop={loop}
|
loop={loop}
|
||||||
showToolbar={showToolbar}
|
showToolbar={showToolbar}
|
||||||
showThumbnails={showThumbnails}
|
showThumbnails={showThumbnails}
|
||||||
toolbarExtra={toolbarExtra}
|
toolbarExtra={toolbarExtra}
|
||||||
zoomStep={zoomStep}
|
zoomStep={zoomStep}
|
||||||
minZoom={minZoom}
|
minZoom={minZoom}
|
||||||
maxZoom={maxZoom}
|
maxZoom={maxZoom}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onIndexChange={handleIndexChange}
|
onIndexChange={handleIndexChange}
|
||||||
/>,
|
/>,
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
ImageViewer.displayName = 'ImageViewer'
|
ImageViewer.displayName = 'ImageViewer'
|
||||||
|
|
|
||||||
|
|
@ -1,327 +1,316 @@
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type PointerEvent as ReactPointerEvent,
|
type PointerEvent as ReactPointerEvent,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useConfig } from '../ConfigProvider'
|
import { useConfig } from '../ConfigProvider'
|
||||||
import type { CommonProps } from '../@types/common'
|
import type { CommonProps } from '../@types/common'
|
||||||
|
|
||||||
export interface KnobProps extends CommonProps {
|
export interface KnobProps extends CommonProps {
|
||||||
/** Kontrollü değer */
|
/** Kontrollü değer */
|
||||||
value?: number
|
value?: number
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: number
|
defaultValue?: number
|
||||||
/** Minimum değer. Varsayılan: 0 */
|
/** Minimum değer. Varsayılan: 0 */
|
||||||
min?: number
|
min?: number
|
||||||
/** Maksimum değer. Varsayılan: 100 */
|
/** Maksimum değer. Varsayılan: 100 */
|
||||||
max?: number
|
max?: number
|
||||||
/** Adım büyüklüğü. Varsayılan: 1 */
|
/** Adım büyüklüğü. Varsayılan: 1 */
|
||||||
step?: number
|
step?: number
|
||||||
/** Çap (px). Varsayılan: 100 */
|
/** Çap (px). Varsayılan: 100 */
|
||||||
size?: number
|
size?: number
|
||||||
/** Çizgi kalınlığı (px). Varsayılan: 14 */
|
/** Çizgi kalınlığı (px). Varsayılan: 14 */
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
/** Değer yayının rengi. Varsayılan: tema rengi */
|
/** Değer yayının rengi. Varsayılan: tema rengi */
|
||||||
valueColor?: string
|
valueColor?: string
|
||||||
/** Arka plan yayının rengi */
|
/** Arka plan yayının rengi */
|
||||||
rangeColor?: string
|
rangeColor?: string
|
||||||
/** Etiket metni rengi */
|
/** Etiket metni rengi */
|
||||||
textColor?: string
|
textColor?: string
|
||||||
/** Değer şablonu. '{value}' placeholder'ı. Varsayılan: '{value}' */
|
/** Değer şablonu. '{value}' placeholder'ı. Varsayılan: '{value}' */
|
||||||
valueTemplate?: string
|
valueTemplate?: string
|
||||||
/** Salt okunur */
|
/** Salt okunur */
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Etiket göster. Varsayılan: true */
|
/** Etiket göster. Varsayılan: true */
|
||||||
showValue?: boolean
|
showValue?: boolean
|
||||||
/** Değer değiştiğinde */
|
/** Değer değiştiğinde */
|
||||||
onChange?: (value: number) => void
|
onChange?: (value: number) => void
|
||||||
/** Input adı (hidden) */
|
/** Input adı (hidden) */
|
||||||
name?: string
|
name?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
field?: any
|
field?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const KNOB_START_ANGLE = -220 // derece (saat 7 hizası)
|
const KNOB_START_ANGLE = -220 // derece (saat 7 hizası)
|
||||||
const KNOB_END_ANGLE = 40 // derece (saat 5 hizası)
|
const KNOB_END_ANGLE = 40 // derece (saat 5 hizası)
|
||||||
const KNOB_RANGE = KNOB_END_ANGLE - KNOB_START_ANGLE // 260 derece toplam yay
|
const KNOB_RANGE = KNOB_END_ANGLE - KNOB_START_ANGLE // 260 derece toplam yay
|
||||||
|
|
||||||
const clamp = (v: number, min: number, max: number) =>
|
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
|
||||||
Math.min(max, Math.max(min, v))
|
|
||||||
|
|
||||||
const snap = (v: number, step: number, min: number) =>
|
const snap = (v: number, step: number, min: number) => Math.round((v - min) / step) * step + min
|
||||||
Math.round((v - min) / step) * step + min
|
|
||||||
|
|
||||||
const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
|
const Knob = forwardRef<SVGSVGElement, KnobProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
defaultValue = 0,
|
defaultValue = 0,
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 100,
|
max = 100,
|
||||||
step = 1,
|
step = 1,
|
||||||
size = 100,
|
size = 100,
|
||||||
strokeWidth = 14,
|
strokeWidth = 14,
|
||||||
valueColor,
|
valueColor,
|
||||||
rangeColor,
|
rangeColor,
|
||||||
textColor,
|
textColor,
|
||||||
valueTemplate = '{value}',
|
valueTemplate = '{value}',
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
showValue = true,
|
showValue = true,
|
||||||
onChange,
|
onChange,
|
||||||
name,
|
name,
|
||||||
field,
|
field,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { themeColor, primaryColorLevel } = useConfig()
|
const { themeColor, primaryColorLevel } = useConfig()
|
||||||
const isControlled = valueProp !== undefined
|
const isControlled = valueProp !== undefined
|
||||||
const [internalValue, setInternalValue] = useState(
|
const [internalValue, setInternalValue] = useState(
|
||||||
clamp(field?.value ?? (isControlled ? valueProp! : defaultValue), min, max),
|
clamp(field?.value ?? (isControlled ? valueProp! : defaultValue), min, max),
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled) setInternalValue(clamp(valueProp!, min, max))
|
if (isControlled) setInternalValue(clamp(valueProp!, min, max))
|
||||||
}, [isControlled, valueProp, min, max])
|
}, [isControlled, valueProp, min, max])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field?.value !== undefined)
|
if (field?.value !== undefined) setInternalValue(clamp(field.value, min, max))
|
||||||
setInternalValue(clamp(field.value, min, max))
|
}, [field?.value, min, max])
|
||||||
}, [field?.value, min, max])
|
|
||||||
|
|
||||||
const currentValue = isControlled
|
const currentValue = isControlled ? clamp(valueProp!, min, max) : internalValue
|
||||||
? clamp(valueProp!, min, max)
|
|
||||||
: internalValue
|
|
||||||
|
|
||||||
const commit = useCallback(
|
const commit = useCallback(
|
||||||
(raw: number) => {
|
(raw: number) => {
|
||||||
const snapped = clamp(snap(raw, step, min), min, max)
|
const snapped = clamp(snap(raw, step, min), min, max)
|
||||||
if (!isControlled) setInternalValue(snapped)
|
if (!isControlled) setInternalValue(snapped)
|
||||||
field?.onChange?.(snapped)
|
field?.onChange?.(snapped)
|
||||||
onChange?.(snapped)
|
onChange?.(snapped)
|
||||||
},
|
},
|
||||||
[isControlled, step, min, max, field, onChange],
|
[isControlled, step, min, max, field, onChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
// SVG geometri
|
// SVG geometri
|
||||||
const r = (size - strokeWidth) / 2
|
const r = (size - strokeWidth) / 2
|
||||||
const cx = size / 2
|
const cx = size / 2
|
||||||
const cy = size / 2
|
const cy = size / 2
|
||||||
const circumference = 2 * Math.PI * r
|
const circumference = 2 * Math.PI * r
|
||||||
|
|
||||||
const valueRatio = (currentValue - min) / (max - min)
|
const valueRatio = (currentValue - min) / (max - min)
|
||||||
const valueAngleDeg = KNOB_START_ANGLE + valueRatio * KNOB_RANGE
|
const valueAngleDeg = KNOB_START_ANGLE + valueRatio * KNOB_RANGE
|
||||||
const valueArcLength = (valueRatio * KNOB_RANGE / 360) * circumference
|
const valueArcLength = ((valueRatio * KNOB_RANGE) / 360) * circumference
|
||||||
const fullArcLength = (KNOB_RANGE / 360) * circumference
|
const fullArcLength = (KNOB_RANGE / 360) * circumference
|
||||||
|
|
||||||
// Çember yayını SVG stroke-dashoffset ile çiziyoruz
|
// Çember yayını SVG stroke-dashoffset ile çiziyoruz
|
||||||
// SVG default: 0 derece = sağ (3 saat), CW pozitif
|
// SVG default: 0 derece = sağ (3 saat), CW pozitif
|
||||||
// Bizim 0 = üst → rotate(-90) + açı offset
|
// Bizim 0 = üst → rotate(-90) + açı offset
|
||||||
const startAngleRad = ((KNOB_START_ANGLE - 90) * Math.PI) / 180
|
const startAngleRad = ((KNOB_START_ANGLE - 90) * Math.PI) / 180
|
||||||
|
|
||||||
const resolveValueColor = () =>
|
const resolveValueColor = () =>
|
||||||
valueColor ?? `var(--color-${themeColor}-${primaryColorLevel}, #6366f1)`
|
valueColor ?? `var(--color-${themeColor}-${primaryColorLevel}, #6366f1)`
|
||||||
const resolveRangeColor = () =>
|
const resolveRangeColor = () => rangeColor ?? `currentColor`
|
||||||
rangeColor ?? `currentColor`
|
const resolveTextColor = () => textColor ?? undefined
|
||||||
const resolveTextColor = () => textColor ?? undefined
|
|
||||||
|
|
||||||
const label = valueTemplate.replace('{value}', String(currentValue))
|
const label = valueTemplate.replace('{value}', String(currentValue))
|
||||||
|
|
||||||
// Pointer/drag etkileşimi
|
// Pointer/drag etkileşimi
|
||||||
const svgRef = useRef<SVGSVGElement>(null)
|
const svgRef = useRef<SVGSVGElement>(null)
|
||||||
const dragging = useRef(false)
|
const dragging = useRef(false)
|
||||||
|
|
||||||
const angleToValue = useCallback(
|
const angleToValue = useCallback(
|
||||||
(angleDeg: number) => {
|
(angleDeg: number) => {
|
||||||
// Normalize to [KNOB_START_ANGLE, KNOB_END_ANGLE]
|
// Normalize to [KNOB_START_ANGLE, KNOB_END_ANGLE]
|
||||||
let a = angleDeg - KNOB_START_ANGLE
|
let a = angleDeg - KNOB_START_ANGLE
|
||||||
if (a < 0) a = 0
|
if (a < 0) a = 0
|
||||||
if (a > KNOB_RANGE) a = KNOB_RANGE
|
if (a > KNOB_RANGE) a = KNOB_RANGE
|
||||||
return min + (a / KNOB_RANGE) * (max - min)
|
return min + (a / KNOB_RANGE) * (max - min)
|
||||||
},
|
},
|
||||||
[min, max],
|
[min, max],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getAngleFromPointer = useCallback(
|
const getAngleFromPointer = useCallback((clientX: number, clientY: number) => {
|
||||||
(clientX: number, clientY: number) => {
|
const el = svgRef.current
|
||||||
const el = svgRef.current
|
if (!el) return 0
|
||||||
if (!el) return 0
|
const rect = el.getBoundingClientRect()
|
||||||
const rect = el.getBoundingClientRect()
|
const centerX = rect.left + rect.width / 2
|
||||||
const centerX = rect.left + rect.width / 2
|
const centerY = rect.top + rect.height / 2
|
||||||
const centerY = rect.top + rect.height / 2
|
const dx = clientX - centerX
|
||||||
const dx = clientX - centerX
|
const dy = clientY - centerY
|
||||||
const dy = clientY - centerY
|
// atan2 → derece, SVG 0=sağ → 90 ekle için 90 çıkarıyoruz
|
||||||
// atan2 → derece, SVG 0=sağ → 90 ekle için 90 çıkarıyoruz
|
let angle = (Math.atan2(dy, dx) * 180) / Math.PI + 90
|
||||||
let angle = (Math.atan2(dy, dx) * 180) / Math.PI + 90
|
// Normalize: KNOB_START_ANGLE -220 → üst-sol
|
||||||
// Normalize: KNOB_START_ANGLE -220 → üst-sol
|
if (angle < KNOB_START_ANGLE + 360) {
|
||||||
if (angle < KNOB_START_ANGLE + 360) {
|
// Döngü düzeltmesi
|
||||||
// Döngü düzeltmesi
|
|
||||||
}
|
|
||||||
return angle
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => {
|
|
||||||
if (disabled || readOnly) return
|
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
|
||||||
dragging.current = true
|
|
||||||
const angle = getAngleFromPointer(e.clientX, e.clientY)
|
|
||||||
commit(angleToValue(angle))
|
|
||||||
}
|
}
|
||||||
|
return angle
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handlePointerMove = (e: ReactPointerEvent<SVGSVGElement>) => {
|
const handlePointerDown = (e: ReactPointerEvent<SVGSVGElement>) => {
|
||||||
if (!dragging.current || disabled || readOnly) return
|
if (disabled || readOnly) return
|
||||||
const angle = getAngleFromPointer(e.clientX, e.clientY)
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
commit(angleToValue(angle))
|
dragging.current = true
|
||||||
|
const angle = getAngleFromPointer(e.clientX, e.clientY)
|
||||||
|
commit(angleToValue(angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerMove = (e: ReactPointerEvent<SVGSVGElement>) => {
|
||||||
|
if (!dragging.current || disabled || readOnly) return
|
||||||
|
const angle = getAngleFromPointer(e.clientX, e.clientY)
|
||||||
|
commit(angleToValue(angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerUp = () => {
|
||||||
|
dragging.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<SVGSVGElement>) => {
|
||||||
|
if (disabled || readOnly) return
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(currentValue + step)
|
||||||
|
break
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(currentValue - step)
|
||||||
|
break
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(min)
|
||||||
|
break
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(max)
|
||||||
|
break
|
||||||
|
case 'PageUp':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(currentValue + step * 10)
|
||||||
|
break
|
||||||
|
case 'PageDown':
|
||||||
|
e.preventDefault()
|
||||||
|
commit(currentValue - step * 10)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
// Stroke-dasharray/offset hesaplama
|
||||||
dragging.current = false
|
// Toplam çevre üzerinde KNOB_RANGE kadar gösteriyoruz
|
||||||
}
|
const dashArray = `${fullArcLength} ${circumference}`
|
||||||
|
const valueDash = `${valueArcLength} ${circumference}`
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<SVGSVGElement>) => {
|
// Rotation: SVG'de 0 derece sağ → başlangıç açısını ayarlamak için rotate uygulayacağız
|
||||||
if (disabled || readOnly) return
|
const trackRotation = `rotate(${KNOB_START_ANGLE + 90}, ${cx}, ${cy})`
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowRight':
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(currentValue + step)
|
|
||||||
break
|
|
||||||
case 'ArrowLeft':
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(currentValue - step)
|
|
||||||
break
|
|
||||||
case 'Home':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(min)
|
|
||||||
break
|
|
||||||
case 'End':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(max)
|
|
||||||
break
|
|
||||||
case 'PageUp':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(currentValue + step * 10)
|
|
||||||
break
|
|
||||||
case 'PageDown':
|
|
||||||
e.preventDefault()
|
|
||||||
commit(currentValue - step * 10)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stroke-dasharray/offset hesaplama
|
return (
|
||||||
// Toplam çevre üzerinde KNOB_RANGE kadar gösteriyoruz
|
<span className={classNames('knob-wrapper', className)} style={style}>
|
||||||
const dashArray = `${fullArcLength} ${circumference}`
|
{name && <input type="hidden" name={name ?? field?.name} value={currentValue} />}
|
||||||
const valueDash = `${valueArcLength} ${circumference}`
|
<svg
|
||||||
|
ref={(node) => {
|
||||||
// Rotation: SVG'de 0 derece sağ → başlangıç açısını ayarlamak için rotate uygulayacağız
|
;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node
|
||||||
const trackRotation = `rotate(${KNOB_START_ANGLE + 90}, ${cx}, ${cy})`
|
if (typeof ref === 'function') ref(node)
|
||||||
|
else if (ref) ref.current = node
|
||||||
return (
|
}}
|
||||||
<span className={classNames('knob-wrapper', className)} style={style}>
|
width={size}
|
||||||
{name && (
|
height={size}
|
||||||
<input type="hidden" name={name ?? field?.name} value={currentValue} />
|
viewBox={`0 0 ${size} ${size}`}
|
||||||
)}
|
className={classNames(
|
||||||
<svg
|
'knob',
|
||||||
ref={(node) => {
|
!disabled && !readOnly && 'knob-interactive',
|
||||||
;(svgRef as React.MutableRefObject<SVGSVGElement | null>).current = node
|
disabled && 'knob-disabled',
|
||||||
if (typeof ref === 'function') ref(node)
|
)}
|
||||||
else if (ref) ref.current = node
|
role="slider"
|
||||||
}}
|
aria-valuemin={min}
|
||||||
width={size}
|
aria-valuemax={max}
|
||||||
height={size}
|
aria-valuenow={currentValue}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
aria-valuetext={label}
|
||||||
className={classNames(
|
aria-disabled={disabled}
|
||||||
'knob',
|
aria-readonly={readOnly}
|
||||||
!disabled && !readOnly && 'knob-interactive',
|
tabIndex={disabled || readOnly ? -1 : 0}
|
||||||
disabled && 'knob-disabled',
|
onPointerDown={handlePointerDown}
|
||||||
)}
|
onPointerMove={handlePointerMove}
|
||||||
role="slider"
|
onPointerUp={handlePointerUp}
|
||||||
aria-valuemin={min}
|
onKeyDown={handleKeyDown}
|
||||||
aria-valuemax={max}
|
{...rest}
|
||||||
aria-valuenow={currentValue}
|
>
|
||||||
aria-valuetext={label}
|
{/* Arka plan yayı */}
|
||||||
aria-disabled={disabled}
|
<circle
|
||||||
aria-readonly={readOnly}
|
cx={cx}
|
||||||
tabIndex={disabled || readOnly ? -1 : 0}
|
cy={cy}
|
||||||
onPointerDown={handlePointerDown}
|
r={r}
|
||||||
onPointerMove={handlePointerMove}
|
fill="none"
|
||||||
onPointerUp={handlePointerUp}
|
stroke={resolveRangeColor()}
|
||||||
onKeyDown={handleKeyDown}
|
strokeWidth={strokeWidth}
|
||||||
{...rest}
|
strokeLinecap="round"
|
||||||
>
|
strokeDasharray={dashArray}
|
||||||
{/* Arka plan yayı */}
|
transform={trackRotation}
|
||||||
<circle
|
className="knob-range"
|
||||||
cx={cx}
|
/>
|
||||||
cy={cy}
|
{/* Değer yayı */}
|
||||||
r={r}
|
{valueArcLength > 0 && (
|
||||||
fill="none"
|
<circle
|
||||||
stroke={resolveRangeColor()}
|
cx={cx}
|
||||||
strokeWidth={strokeWidth}
|
cy={cy}
|
||||||
strokeLinecap="round"
|
r={r}
|
||||||
strokeDasharray={dashArray}
|
fill="none"
|
||||||
transform={trackRotation}
|
stroke={resolveValueColor()}
|
||||||
className="knob-range"
|
strokeWidth={strokeWidth}
|
||||||
/>
|
strokeLinecap="round"
|
||||||
{/* Değer yayı */}
|
strokeDasharray={valueDash}
|
||||||
{valueArcLength > 0 && (
|
transform={trackRotation}
|
||||||
<circle
|
className="knob-value-arc"
|
||||||
cx={cx}
|
/>
|
||||||
cy={cy}
|
)}
|
||||||
r={r}
|
{/* Handle noktası */}
|
||||||
fill="none"
|
{(() => {
|
||||||
stroke={resolveValueColor()}
|
const handleAngle = (valueAngleDeg - 90) * (Math.PI / 180)
|
||||||
strokeWidth={strokeWidth}
|
const hx = cx + r * Math.cos(handleAngle)
|
||||||
strokeLinecap="round"
|
const hy = cy + r * Math.sin(handleAngle)
|
||||||
strokeDasharray={valueDash}
|
return (
|
||||||
transform={trackRotation}
|
<circle
|
||||||
className="knob-value-arc"
|
cx={hx}
|
||||||
/>
|
cy={hy}
|
||||||
)}
|
r={strokeWidth / 2 + 1}
|
||||||
{/* Handle noktası */}
|
fill={resolveValueColor()}
|
||||||
{(() => {
|
className="knob-handle"
|
||||||
const handleAngle = (valueAngleDeg - 90) * (Math.PI / 180)
|
/>
|
||||||
const hx = cx + r * Math.cos(handleAngle)
|
)
|
||||||
const hy = cy + r * Math.sin(handleAngle)
|
})()}
|
||||||
return (
|
{/* Etiket */}
|
||||||
<circle
|
{showValue && (
|
||||||
cx={hx}
|
<text
|
||||||
cy={hy}
|
x={cx}
|
||||||
r={strokeWidth / 2 + 1}
|
y={cy}
|
||||||
fill={resolveValueColor()}
|
textAnchor="middle"
|
||||||
className="knob-handle"
|
dominantBaseline="central"
|
||||||
/>
|
fontSize={size * 0.2}
|
||||||
)
|
fontWeight={600}
|
||||||
})()}
|
fill={resolveTextColor()}
|
||||||
{/* Etiket */}
|
className="knob-label"
|
||||||
{showValue && (
|
>
|
||||||
<text
|
{label}
|
||||||
x={cx}
|
</text>
|
||||||
y={cy}
|
)}
|
||||||
textAnchor="middle"
|
</svg>
|
||||||
dominantBaseline="central"
|
</span>
|
||||||
fontSize={size * 0.2}
|
)
|
||||||
fontWeight={600}
|
|
||||||
fill={resolveTextColor()}
|
|
||||||
className="knob-label"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Knob.displayName = 'Knob'
|
Knob.displayName = 'Knob'
|
||||||
|
|
|
||||||
|
|
@ -1,219 +1,201 @@
|
||||||
import React, {
|
import React, { forwardRef, useRef, useState, useEffect, useCallback, Fragment } from 'react'
|
||||||
forwardRef,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
Fragment,
|
|
||||||
} from 'react'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import type { CommonProps } from '../@types/common'
|
import type { CommonProps } from '../@types/common'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
export interface MarqueeProps extends CommonProps {
|
export interface MarqueeProps extends CommonProps {
|
||||||
/** Oynatma hızı (piksel/saniye). Varsayılan: 50 */
|
/** Oynatma hızı (piksel/saniye). Varsayılan: 50 */
|
||||||
speed?: number
|
speed?: number
|
||||||
/** Kaydırma yönü. Varsayılan: 'left' */
|
/** Kaydırma yönü. Varsayılan: 'left' */
|
||||||
direction?: 'left' | 'right' | 'up' | 'down'
|
direction?: 'left' | 'right' | 'up' | 'down'
|
||||||
/** Fare üzerindeyken duraklat. Varsayılan: false */
|
/** Fare üzerindeyken duraklat. Varsayılan: false */
|
||||||
pauseOnHover?: boolean
|
pauseOnHover?: boolean
|
||||||
/** Tıklandığında duraklat. Varsayılan: false */
|
/** Tıklandığında duraklat. Varsayılan: false */
|
||||||
pauseOnClick?: boolean
|
pauseOnClick?: boolean
|
||||||
/** Döngü sayısı. 0 = sonsuz. Varsayılan: 0 */
|
/** Döngü sayısı. 0 = sonsuz. Varsayılan: 0 */
|
||||||
loop?: number
|
loop?: number
|
||||||
/** Kenar degradesi göster. Varsayılan: true */
|
/** Kenar degradesi göster. Varsayılan: true */
|
||||||
gradient?: boolean
|
gradient?: boolean
|
||||||
/** Degrade rengi (rgb). Varsayılan: '255,255,255' */
|
/** Degrade rengi (rgb). Varsayılan: '255,255,255' */
|
||||||
gradientColor?: string
|
gradientColor?: string
|
||||||
/** Degrade genişliği. Varsayılan: 200 */
|
/** Degrade genişliği. Varsayılan: 200 */
|
||||||
gradientWidth?: number | string
|
gradientWidth?: number | string
|
||||||
/** Başlangıç gecikmesi (ms). Varsayılan: 0 */
|
/** Başlangıç gecikmesi (ms). Varsayılan: 0 */
|
||||||
delay?: number
|
delay?: number
|
||||||
/** Oynatma durumu. Varsayılan: true */
|
/** Oynatma durumu. Varsayılan: true */
|
||||||
play?: boolean
|
play?: boolean
|
||||||
/** İçeriği otomatik doldur. Varsayılan: false */
|
/** İçeriği otomatik doldur. Varsayılan: false */
|
||||||
autoFill?: boolean
|
autoFill?: boolean
|
||||||
/** Animasyon tamamlandığında callback */
|
/** Animasyon tamamlandığında callback */
|
||||||
onFinish?: () => void
|
onFinish?: () => void
|
||||||
/** Döngü tamamlandığında callback */
|
/** Döngü tamamlandığında callback */
|
||||||
onCycleComplete?: () => void
|
onCycleComplete?: () => void
|
||||||
/** Oynatma başladığında callback */
|
/** Oynatma başladığında callback */
|
||||||
onMount?: () => void
|
onMount?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
|
const Marquee = forwardRef<HTMLDivElement, MarqueeProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
speed = 50,
|
speed = 50,
|
||||||
direction = 'left',
|
direction = 'left',
|
||||||
pauseOnHover = false,
|
pauseOnHover = false,
|
||||||
pauseOnClick = false,
|
pauseOnClick = false,
|
||||||
loop = 0,
|
loop = 0,
|
||||||
gradient = true,
|
gradient = true,
|
||||||
gradientColor = '255,255,255',
|
gradientColor = '255,255,255',
|
||||||
gradientWidth = 200,
|
gradientWidth = 200,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
play = true,
|
play = true,
|
||||||
autoFill = false,
|
autoFill = false,
|
||||||
onFinish,
|
onFinish,
|
||||||
onCycleComplete,
|
onCycleComplete,
|
||||||
onMount,
|
onMount,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const trackRef = useRef<HTMLDivElement>(null)
|
const trackRef = useRef<HTMLDivElement>(null)
|
||||||
const [trackWidth, setTrackWidth] = useState(0)
|
const [trackWidth, setTrackWidth] = useState(0)
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
const [multiplier, setMultiplier] = useState(1)
|
const [multiplier, setMultiplier] = useState(1)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
|
||||||
const isVertical = direction === 'up' || direction === 'down'
|
const isVertical = direction === 'up' || direction === 'down'
|
||||||
const isReverse = direction === 'right' || direction === 'down'
|
const isReverse = direction === 'right' || direction === 'down'
|
||||||
|
|
||||||
const calculateWidth = useCallback(() => {
|
const calculateWidth = useCallback(() => {
|
||||||
if (trackRef.current && containerRef.current) {
|
if (trackRef.current && containerRef.current) {
|
||||||
const tw = isVertical
|
const tw = isVertical ? trackRef.current.offsetHeight : trackRef.current.offsetWidth
|
||||||
? trackRef.current.offsetHeight
|
const cw = isVertical ? containerRef.current.offsetHeight : containerRef.current.offsetWidth
|
||||||
: trackRef.current.offsetWidth
|
|
||||||
const cw = isVertical
|
|
||||||
? containerRef.current.offsetHeight
|
|
||||||
: containerRef.current.offsetWidth
|
|
||||||
|
|
||||||
if (autoFill && tw > 0) {
|
if (autoFill && tw > 0) {
|
||||||
setMultiplier(Math.max(Math.ceil((cw * 2) / tw), 2))
|
setMultiplier(Math.max(Math.ceil((cw * 2) / tw), 2))
|
||||||
} else {
|
} else {
|
||||||
setMultiplier(1)
|
setMultiplier(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrackWidth(tw)
|
setTrackWidth(tw)
|
||||||
setContainerWidth(cw)
|
setContainerWidth(cw)
|
||||||
|
}
|
||||||
|
}, [autoFill, isVertical])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculateWidth()
|
||||||
|
const resizeObserver = new ResizeObserver(calculateWidth)
|
||||||
|
if (containerRef.current) resizeObserver.observe(containerRef.current)
|
||||||
|
if (trackRef.current) resizeObserver.observe(trackRef.current)
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
|
}, [calculateWidth, children])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true)
|
||||||
|
onMount?.()
|
||||||
|
}, [onMount])
|
||||||
|
|
||||||
|
const duration =
|
||||||
|
trackWidth > 0
|
||||||
|
? autoFill
|
||||||
|
? (trackWidth * multiplier) / speed
|
||||||
|
: Math.max(trackWidth, containerWidth) / speed
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const gradientStyle: CSSProperties = gradient
|
||||||
|
? {
|
||||||
|
['--marquee-gradient-color' as string]: `rgba(${gradientColor}, 1), rgba(${gradientColor}, 0)`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const containerStyle: CSSProperties = {
|
||||||
|
...gradientStyle,
|
||||||
|
...style,
|
||||||
|
['--marquee-gradient-width' as string]:
|
||||||
|
typeof gradientWidth === 'number' ? `${gradientWidth}px` : gradientWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
const animationStyle: CSSProperties = {
|
||||||
|
['--marquee-duration' as string]: `${duration}s`,
|
||||||
|
['--marquee-delay' as string]: `${delay}s`,
|
||||||
|
['--marquee-iteration-count' as string]: loop > 0 ? `${loop}` : 'infinite',
|
||||||
|
}
|
||||||
|
|
||||||
|
const animationClass = isVertical
|
||||||
|
? isReverse
|
||||||
|
? 'marquee-animate-down'
|
||||||
|
: 'marquee-animate-up'
|
||||||
|
: isReverse
|
||||||
|
? 'marquee-animate-right'
|
||||||
|
: 'marquee-animate-left'
|
||||||
|
|
||||||
|
const isPaused = !play
|
||||||
|
|
||||||
|
const handleAnimationIteration = () => {
|
||||||
|
onCycleComplete?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
onFinish?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedItems = Array.from({ length: multiplier }, (_, i) => (
|
||||||
|
<Fragment key={i}>{children}</Fragment>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(node) => {
|
||||||
|
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(node)
|
||||||
|
} else if (ref) {
|
||||||
|
ref.current = node
|
||||||
}
|
}
|
||||||
}, [autoFill, isVertical])
|
}}
|
||||||
|
className={classNames(
|
||||||
useEffect(() => {
|
'marquee-container',
|
||||||
calculateWidth()
|
isVertical && 'marquee-vertical',
|
||||||
const resizeObserver = new ResizeObserver(calculateWidth)
|
gradient && 'marquee-gradient',
|
||||||
if (containerRef.current) resizeObserver.observe(containerRef.current)
|
className,
|
||||||
if (trackRef.current) resizeObserver.observe(trackRef.current)
|
)}
|
||||||
return () => resizeObserver.disconnect()
|
style={containerStyle}
|
||||||
}, [calculateWidth, children])
|
{...rest}
|
||||||
|
>
|
||||||
useEffect(() => {
|
{isMounted && (
|
||||||
setIsMounted(true)
|
<>
|
||||||
onMount?.()
|
<div
|
||||||
}, [onMount])
|
ref={trackRef}
|
||||||
|
|
||||||
const duration =
|
|
||||||
trackWidth > 0
|
|
||||||
? autoFill
|
|
||||||
? (trackWidth * multiplier) / speed
|
|
||||||
: Math.max(trackWidth, containerWidth) / speed
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const gradientStyle: CSSProperties = gradient
|
|
||||||
? {
|
|
||||||
['--marquee-gradient-color' as string]: `rgba(${gradientColor}, 1), rgba(${gradientColor}, 0)`,
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
|
|
||||||
const containerStyle: CSSProperties = {
|
|
||||||
...gradientStyle,
|
|
||||||
...style,
|
|
||||||
['--marquee-gradient-width' as string]:
|
|
||||||
typeof gradientWidth === 'number'
|
|
||||||
? `${gradientWidth}px`
|
|
||||||
: gradientWidth,
|
|
||||||
}
|
|
||||||
|
|
||||||
const animationStyle: CSSProperties = {
|
|
||||||
['--marquee-duration' as string]: `${duration}s`,
|
|
||||||
['--marquee-delay' as string]: `${delay}s`,
|
|
||||||
['--marquee-iteration-count' as string]: loop > 0 ? `${loop}` : 'infinite',
|
|
||||||
}
|
|
||||||
|
|
||||||
const animationClass = isVertical
|
|
||||||
? isReverse
|
|
||||||
? 'marquee-animate-down'
|
|
||||||
: 'marquee-animate-up'
|
|
||||||
: isReverse
|
|
||||||
? 'marquee-animate-right'
|
|
||||||
: 'marquee-animate-left'
|
|
||||||
|
|
||||||
const isPaused = !play
|
|
||||||
|
|
||||||
const handleAnimationIteration = () => {
|
|
||||||
onCycleComplete?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAnimationEnd = () => {
|
|
||||||
onFinish?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const clonedItems = Array.from({ length: multiplier }, (_, i) => (
|
|
||||||
<Fragment key={i}>{children}</Fragment>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(node) => {
|
|
||||||
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current =
|
|
||||||
node
|
|
||||||
if (typeof ref === 'function') {
|
|
||||||
ref(node)
|
|
||||||
} else if (ref) {
|
|
||||||
ref.current = node
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'marquee-container',
|
'marquee-track',
|
||||||
isVertical && 'marquee-vertical',
|
animationClass,
|
||||||
gradient && 'marquee-gradient',
|
(pauseOnHover || isPaused) && 'marquee-pause-on-hover',
|
||||||
className,
|
(pauseOnClick || isPaused) && isPaused && 'marquee-paused',
|
||||||
)}
|
)}
|
||||||
style={containerStyle}
|
style={animationStyle}
|
||||||
{...rest}
|
onAnimationIteration={handleAnimationIteration}
|
||||||
>
|
onAnimationEnd={handleAnimationEnd}
|
||||||
{isMounted && (
|
aria-hidden={multiplier > 1}
|
||||||
<>
|
>
|
||||||
<div
|
{clonedItems}
|
||||||
ref={trackRef}
|
</div>
|
||||||
className={classNames(
|
{/* Klonlanmış track - kesintisiz görünüm için */}
|
||||||
'marquee-track',
|
<div
|
||||||
animationClass,
|
className={classNames(
|
||||||
(pauseOnHover || isPaused) &&
|
'marquee-track',
|
||||||
'marquee-pause-on-hover',
|
animationClass,
|
||||||
(pauseOnClick || isPaused) &&
|
(pauseOnHover || isPaused) && 'marquee-pause-on-hover',
|
||||||
isPaused &&
|
isPaused && 'marquee-paused',
|
||||||
'marquee-paused',
|
|
||||||
)}
|
|
||||||
style={animationStyle}
|
|
||||||
onAnimationIteration={handleAnimationIteration}
|
|
||||||
onAnimationEnd={handleAnimationEnd}
|
|
||||||
aria-hidden={multiplier > 1}
|
|
||||||
>
|
|
||||||
{clonedItems}
|
|
||||||
</div>
|
|
||||||
{/* Klonlanmış track - kesintisiz görünüm için */}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'marquee-track',
|
|
||||||
animationClass,
|
|
||||||
(pauseOnHover || isPaused) &&
|
|
||||||
'marquee-pause-on-hover',
|
|
||||||
isPaused && 'marquee-paused',
|
|
||||||
)}
|
|
||||||
style={animationStyle}
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
{clonedItems}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
style={animationStyle}
|
||||||
)
|
aria-hidden
|
||||||
|
>
|
||||||
|
{clonedItems}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Marquee.displayName = 'Marquee'
|
Marquee.displayName = 'Marquee'
|
||||||
|
|
|
||||||
|
|
@ -7,109 +7,84 @@ import type { CommonProps, TypeAttributes } from '../@types/common'
|
||||||
import type { ReactNode, MouseEvent } from 'react'
|
import type { ReactNode, MouseEvent } from 'react'
|
||||||
|
|
||||||
export interface NotificationProps extends CommonProps {
|
export interface NotificationProps extends CommonProps {
|
||||||
closable?: boolean
|
closable?: boolean
|
||||||
customIcon?: ReactNode | string
|
customIcon?: ReactNode | string
|
||||||
duration?: number
|
duration?: number
|
||||||
onClose?: (e: MouseEvent<HTMLSpanElement>) => void
|
onClose?: (e: MouseEvent<HTMLSpanElement>) => void
|
||||||
title?: string
|
title?: string
|
||||||
triggerByToast?: boolean
|
triggerByToast?: boolean
|
||||||
type?: TypeAttributes.Status
|
type?: TypeAttributes.Status
|
||||||
width?: number | string
|
width?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notification = forwardRef<HTMLDivElement, NotificationProps>(
|
const Notification = forwardRef<HTMLDivElement, NotificationProps>((props, ref) => {
|
||||||
(props, ref) => {
|
const {
|
||||||
const {
|
className,
|
||||||
className,
|
children,
|
||||||
children,
|
closable = false,
|
||||||
closable = false,
|
customIcon,
|
||||||
customIcon,
|
duration = 3000,
|
||||||
duration = 3000,
|
onClose,
|
||||||
onClose,
|
style,
|
||||||
style,
|
title,
|
||||||
title,
|
triggerByToast,
|
||||||
triggerByToast,
|
type,
|
||||||
type,
|
width = 350,
|
||||||
width = 350,
|
...rest
|
||||||
...rest
|
} = props
|
||||||
} = props
|
|
||||||
|
|
||||||
// Toast tarafından yönetilmeyen bildirimler kapatma animasyonu
|
// Toast tarafından yönetilmeyen bildirimler kapatma animasyonu
|
||||||
// tamamlandıktan sonra kendilerini DOM'dan kaldırır.
|
// tamamlandıktan sonra kendilerini DOM'dan kaldırır.
|
||||||
const [hidden, setHidden] = useState(false)
|
const [hidden, setHidden] = useState(false)
|
||||||
|
|
||||||
const { clear } = useTimeout(
|
const { clear } = useTimeout(onClose as () => void, duration, duration > 0)
|
||||||
onClose as () => void,
|
|
||||||
duration,
|
|
||||||
duration > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(
|
||||||
(e: MouseEvent<HTMLSpanElement>) => {
|
(e: MouseEvent<HTMLSpanElement>) => {
|
||||||
onClose?.(e)
|
onClose?.(e)
|
||||||
clear()
|
clear()
|
||||||
if (!triggerByToast) {
|
if (!triggerByToast) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setHidden(true)
|
setHidden(true)
|
||||||
}, 400)
|
}, 400)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onClose, clear, triggerByToast]
|
[onClose, clear, triggerByToast],
|
||||||
)
|
)
|
||||||
|
|
||||||
const notificationClass = classNames('notification', className)
|
const notificationClass = classNames('notification', className)
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} {...rest} className={notificationClass} style={{ width: width, ...style }}>
|
||||||
ref={ref}
|
<div className={classNames('notification-content', !children && 'no-child')}>
|
||||||
{...rest}
|
{type && !customIcon ? (
|
||||||
className={notificationClass}
|
<div className="mr-3">
|
||||||
style={{ width: width, ...style }}
|
<StatusIcon type={type} />
|
||||||
>
|
</div>
|
||||||
<div
|
) : null}
|
||||||
className={classNames(
|
{customIcon && <div className="mr-3">{customIcon}</div>}
|
||||||
'notification-content',
|
<div className="mr-4">
|
||||||
!children && 'no-child'
|
{title && (
|
||||||
)}
|
<div className={classNames('notification-title', !!children && 'mb-1')}>{title}</div>
|
||||||
>
|
)}
|
||||||
{type && !customIcon ? (
|
<div className="notification-description">{children}</div>
|
||||||
<div className="mr-3">
|
</div>
|
||||||
<StatusIcon type={type} />
|
</div>
|
||||||
</div>
|
{closable && (
|
||||||
) : null}
|
<CloseButton
|
||||||
{customIcon && <div className="mr-3">{customIcon}</div>}
|
className="notification-close"
|
||||||
<div className="mr-4">
|
defaultStyle={false}
|
||||||
{title && (
|
absolute={true}
|
||||||
<div
|
onClick={handleClose}
|
||||||
className={classNames(
|
/>
|
||||||
'notification-title',
|
)}
|
||||||
!!children && 'mb-1'
|
</div>
|
||||||
)}
|
)
|
||||||
>
|
})
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="notification-description">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{closable && (
|
|
||||||
<CloseButton
|
|
||||||
className="notification-close"
|
|
||||||
defaultStyle={false}
|
|
||||||
absolute={true}
|
|
||||||
onClick={handleClose}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Notification.displayName = 'Notification'
|
Notification.displayName = 'Notification'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,229 +1,223 @@
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useConfig } from '../ConfigProvider'
|
import { useConfig } from '../ConfigProvider'
|
||||||
import type { CommonProps } from '../@types/common'
|
import type { CommonProps } from '../@types/common'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export interface RateProps extends CommonProps {
|
export interface RateProps extends CommonProps {
|
||||||
/** Toplam yıldız sayısı. Varsayılan: 5 */
|
/** Toplam yıldız sayısı. Varsayılan: 5 */
|
||||||
count?: number
|
count?: number
|
||||||
/** Kontrollü değer */
|
/** Kontrollü değer */
|
||||||
value?: number
|
value?: number
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: number
|
defaultValue?: number
|
||||||
/** Yarım yıldız desteği. Varsayılan: false */
|
/** Yarım yıldız desteği. Varsayılan: false */
|
||||||
allowHalf?: boolean
|
allowHalf?: boolean
|
||||||
/** Temizlemeye izin ver (aynı yıldıza tıkla = sıfırla). Varsayılan: true */
|
/** Temizlemeye izin ver (aynı yıldıza tıkla = sıfırla). Varsayılan: true */
|
||||||
allowClear?: boolean
|
allowClear?: boolean
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Salt okunur */
|
/** Salt okunur */
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
/** Özel ikon (dolu, boş) */
|
/** Özel ikon (dolu, boş) */
|
||||||
character?: ReactNode | ((index: number) => ReactNode)
|
character?: ReactNode | ((index: number) => ReactNode)
|
||||||
/** Tema rengi. Varsayılan: 'amber' */
|
/** Tema rengi. Varsayılan: 'amber' */
|
||||||
color?: string
|
color?: string
|
||||||
/** Yıldız boyutu (px). Varsayılan: 20 */
|
/** Yıldız boyutu (px). Varsayılan: 20 */
|
||||||
size?: number
|
size?: number
|
||||||
/** Boşluk arası (px). Varsayılan: 4 */
|
/** Boşluk arası (px). Varsayılan: 4 */
|
||||||
gap?: number
|
gap?: number
|
||||||
/** Değer değiştiğinde callback */
|
/** Değer değiştiğinde callback */
|
||||||
onChange?: (value: number) => void
|
onChange?: (value: number) => void
|
||||||
/** Hover değiştiğinde callback */
|
/** Hover değiştiğinde callback */
|
||||||
onHoverChange?: (value: number) => void
|
onHoverChange?: (value: number) => void
|
||||||
/** Tooltip metinleri */
|
/** Tooltip metinleri */
|
||||||
tooltips?: string[]
|
tooltips?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size: number }) => (
|
const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size: number }) => (
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
{half && (
|
{half && (
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="rate-half">
|
<linearGradient id="rate-half">
|
||||||
<stop offset="50%" stopColor="currentColor" />
|
<stop offset="50%" stopColor="currentColor" />
|
||||||
<stop offset="50%" stopColor="transparent" />
|
<stop offset="50%" stopColor="transparent" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
)}
|
)}
|
||||||
<polygon
|
<polygon
|
||||||
points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"
|
points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"
|
||||||
fill={
|
fill={half ? 'url(#rate-half)' : filled ? 'currentColor' : 'none'}
|
||||||
half
|
stroke="currentColor"
|
||||||
? 'url(#rate-half)'
|
strokeWidth="1.5"
|
||||||
: filled
|
strokeLinecap="round"
|
||||||
? 'currentColor'
|
strokeLinejoin="round"
|
||||||
: 'none'
|
/>
|
||||||
}
|
</svg>
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
|
const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
|
||||||
const {
|
const { translate } = useLocalization()
|
||||||
className,
|
const {
|
||||||
style,
|
className,
|
||||||
count = 5,
|
style,
|
||||||
value: valueProp,
|
count = 5,
|
||||||
defaultValue = 0,
|
value: valueProp,
|
||||||
allowHalf = false,
|
defaultValue = 0,
|
||||||
allowClear = true,
|
allowHalf = false,
|
||||||
disabled = false,
|
allowClear = true,
|
||||||
readOnly = false,
|
disabled = false,
|
||||||
character,
|
readOnly = false,
|
||||||
color = 'amber',
|
character,
|
||||||
size = 20,
|
color = 'amber',
|
||||||
gap = 4,
|
size = 20,
|
||||||
onChange,
|
gap = 4,
|
||||||
onHoverChange,
|
onChange,
|
||||||
tooltips,
|
onHoverChange,
|
||||||
...rest
|
tooltips,
|
||||||
} = props
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
const { themeColor, primaryColorLevel } = useConfig()
|
const { themeColor, primaryColorLevel } = useConfig()
|
||||||
const isControlled = valueProp !== undefined
|
const isControlled = valueProp !== undefined
|
||||||
const [internalValue, setInternalValue] = useState(defaultValue)
|
const [internalValue, setInternalValue] = useState(defaultValue)
|
||||||
const [hoverValue, setHoverValue] = useState<number | null>(null)
|
const [hoverValue, setHoverValue] = useState<number | null>(null)
|
||||||
const lastClickedRef = useRef<number | null>(null)
|
const lastClickedRef = useRef<number | null>(null)
|
||||||
|
|
||||||
const value = isControlled ? valueProp! : internalValue
|
const value = isControlled ? valueProp! : internalValue
|
||||||
const displayValue = hoverValue !== null ? hoverValue : value
|
const displayValue = hoverValue !== null ? hoverValue : value
|
||||||
|
|
||||||
const resolveColor = () => {
|
const resolveColor = () => {
|
||||||
if (color === 'theme') return `text-${themeColor}-${primaryColorLevel}`
|
if (color === 'theme') return `text-${themeColor}-${primaryColorLevel}`
|
||||||
return `text-${color}-400`
|
return `text-${color}-400`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStarValue = (index: number, offsetX: number, starWidth: number) => {
|
||||||
|
if (allowHalf && offsetX < starWidth / 2) {
|
||||||
|
return index + 0.5
|
||||||
}
|
}
|
||||||
|
return index + 1
|
||||||
|
}
|
||||||
|
|
||||||
const getStarValue = (index: number, offsetX: number, starWidth: number) => {
|
const handleMouseMove = useCallback(
|
||||||
if (allowHalf && offsetX < starWidth / 2) {
|
(index: number, e: React.MouseEvent<HTMLSpanElement>) => {
|
||||||
return index + 0.5
|
if (disabled || readOnly) return
|
||||||
}
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
return index + 1
|
const offsetX = e.clientX - rect.left
|
||||||
}
|
const newVal = getStarValue(index, offsetX, rect.width)
|
||||||
|
if (newVal !== hoverValue) {
|
||||||
|
setHoverValue(newVal)
|
||||||
|
onHoverChange?.(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, readOnly, hoverValue, allowHalf, onHoverChange],
|
||||||
|
)
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseLeave = useCallback(() => {
|
||||||
(index: number, e: React.MouseEvent<HTMLSpanElement>) => {
|
if (disabled || readOnly) return
|
||||||
if (disabled || readOnly) return
|
setHoverValue(null)
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
onHoverChange?.(0)
|
||||||
const offsetX = e.clientX - rect.left
|
}, [disabled, readOnly, onHoverChange])
|
||||||
const newVal = getStarValue(index, offsetX, rect.width)
|
|
||||||
if (newVal !== hoverValue) {
|
|
||||||
setHoverValue(newVal)
|
|
||||||
onHoverChange?.(newVal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled, readOnly, hoverValue, allowHalf, onHoverChange],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleClick = useCallback(
|
||||||
if (disabled || readOnly) return
|
(index: number, e: React.MouseEvent<HTMLSpanElement>) => {
|
||||||
setHoverValue(null)
|
if (disabled || readOnly) return
|
||||||
onHoverChange?.(0)
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
}, [disabled, readOnly, onHoverChange])
|
const offsetX = e.clientX - rect.left
|
||||||
|
const clickedVal = getStarValue(index, offsetX, rect.width)
|
||||||
|
|
||||||
const handleClick = useCallback(
|
let newVal = clickedVal
|
||||||
(index: number, e: React.MouseEvent<HTMLSpanElement>) => {
|
if (allowClear && clickedVal === lastClickedRef.current) {
|
||||||
if (disabled || readOnly) return
|
newVal = 0
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
lastClickedRef.current = null
|
||||||
const offsetX = e.clientX - rect.left
|
} else {
|
||||||
const clickedVal = getStarValue(index, offsetX, rect.width)
|
lastClickedRef.current = clickedVal
|
||||||
|
}
|
||||||
|
|
||||||
let newVal = clickedVal
|
if (!isControlled) setInternalValue(newVal)
|
||||||
if (allowClear && clickedVal === lastClickedRef.current) {
|
onChange?.(newVal)
|
||||||
newVal = 0
|
},
|
||||||
lastClickedRef.current = null
|
[disabled, readOnly, allowClear, allowHalf, isControlled, onChange],
|
||||||
} else {
|
)
|
||||||
lastClickedRef.current = clickedVal
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isControlled) setInternalValue(newVal)
|
const handleKeyDown = useCallback(
|
||||||
onChange?.(newVal)
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
},
|
if (disabled || readOnly) return
|
||||||
[disabled, readOnly, allowClear, allowHalf, isControlled, onChange],
|
let newVal = value
|
||||||
)
|
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
||||||
|
newVal = Math.min(value + (allowHalf ? 0.5 : 1), count)
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
||||||
|
newVal = Math.max(value - (allowHalf ? 0.5 : 1), 0)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
if (!isControlled) setInternalValue(newVal)
|
||||||
|
onChange?.(newVal)
|
||||||
|
},
|
||||||
|
[disabled, readOnly, value, allowHalf, count, isControlled, onChange],
|
||||||
|
)
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const renderStar = (index: number) => {
|
||||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
const filled = displayValue >= index + 1
|
||||||
if (disabled || readOnly) return
|
const half = !filled && allowHalf && displayValue >= index + 0.5
|
||||||
let newVal = value
|
const tooltip = tooltips?.[index]
|
||||||
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
||||||
newVal = Math.min(value + (allowHalf ? 0.5 : 1), count)
|
|
||||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
||||||
newVal = Math.max(value - (allowHalf ? 0.5 : 1), 0)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
if (!isControlled) setInternalValue(newVal)
|
|
||||||
onChange?.(newVal)
|
|
||||||
},
|
|
||||||
[disabled, readOnly, value, allowHalf, count, isControlled, onChange],
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderStar = (index: number) => {
|
const starNode =
|
||||||
const filled = displayValue >= index + 1
|
typeof character === 'function'
|
||||||
const half = !filled && allowHalf && displayValue >= index + 0.5
|
? character(index)
|
||||||
const tooltip = tooltips?.[index]
|
: (character ?? <StarIcon filled={filled} half={half} size={size} />)
|
||||||
|
|
||||||
const starNode =
|
|
||||||
typeof character === 'function'
|
|
||||||
? character(index)
|
|
||||||
: character ?? (
|
|
||||||
<StarIcon filled={filled} half={half} size={size} />
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className={classNames(
|
|
||||||
'rate-star',
|
|
||||||
filled || half ? resolveColor() : 'text-gray-300 dark:text-gray-600',
|
|
||||||
!disabled && !readOnly && 'cursor-pointer',
|
|
||||||
disabled && 'opacity-50 cursor-not-allowed',
|
|
||||||
)}
|
|
||||||
style={{ marginRight: index < count - 1 ? gap : 0 }}
|
|
||||||
title={tooltip}
|
|
||||||
onMouseMove={(e) => handleMouseMove(index, e)}
|
|
||||||
onClick={(e) => handleClick(index, e)}
|
|
||||||
role={!disabled && !readOnly ? 'radio' : undefined}
|
|
||||||
aria-checked={displayValue >= index + 1}
|
|
||||||
aria-label={tooltip ?? `${index + 1} yıldız`}
|
|
||||||
>
|
|
||||||
{starNode}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<span
|
||||||
ref={ref}
|
key={index}
|
||||||
className={classNames('rate', className)}
|
className={classNames(
|
||||||
style={style}
|
'rate-star',
|
||||||
onMouseLeave={handleMouseLeave}
|
filled || half ? resolveColor() : 'text-gray-300 dark:text-gray-600',
|
||||||
onKeyDown={handleKeyDown}
|
!disabled && !readOnly && 'cursor-pointer',
|
||||||
tabIndex={disabled || readOnly ? undefined : 0}
|
disabled && 'opacity-50 cursor-not-allowed',
|
||||||
role="radiogroup"
|
)}
|
||||||
aria-label="Değerlendirme"
|
style={{ marginRight: index < count - 1 ? gap : 0 }}
|
||||||
{...rest}
|
title={tooltip}
|
||||||
>
|
onMouseMove={(e) => handleMouseMove(index, e)}
|
||||||
{Array.from({ length: count }, (_, i) => renderStar(i))}
|
onClick={(e) => handleClick(index, e)}
|
||||||
</div>
|
role={!disabled && !readOnly ? 'radio' : undefined}
|
||||||
|
aria-checked={displayValue >= index + 1}
|
||||||
|
aria-label={tooltip ?? translate('::App.Rate.StarCount', { count: index + 1 })}
|
||||||
|
>
|
||||||
|
{starNode}
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames('rate', className)}
|
||||||
|
style={style}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={disabled || readOnly ? undefined : 0}
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={translate('::App.Rate.Label')}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{Array.from({ length: count }, (_, i) => renderStar(i))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Rate.displayName = 'Rate'
|
Rate.displayName = 'Rate'
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type PointerEvent as ReactPointerEvent,
|
type PointerEvent as ReactPointerEvent,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useConfig } from '../ConfigProvider'
|
import { useConfig } from '../ConfigProvider'
|
||||||
|
|
@ -14,366 +14,365 @@ import type { CommonProps } from '../@types/common'
|
||||||
export type SliderValue = number | [number, number]
|
export type SliderValue = number | [number, number]
|
||||||
|
|
||||||
export interface SliderProps extends CommonProps {
|
export interface SliderProps extends CommonProps {
|
||||||
/** Kontrollü değer (tek veya range) */
|
/** Kontrollü değer (tek veya range) */
|
||||||
value?: SliderValue
|
value?: SliderValue
|
||||||
/** Başlangıç değeri (kontrolsüz) */
|
/** Başlangıç değeri (kontrolsüz) */
|
||||||
defaultValue?: SliderValue
|
defaultValue?: SliderValue
|
||||||
/** Minimum değer. Varsayılan: 0 */
|
/** Minimum değer. Varsayılan: 0 */
|
||||||
min?: number
|
min?: number
|
||||||
/** Maksimum değer. Varsayılan: 100 */
|
/** Maksimum değer. Varsayılan: 100 */
|
||||||
max?: number
|
max?: number
|
||||||
/** Adım büyüklüğü. Varsayılan: 1 */
|
/** Adım büyüklüğü. Varsayılan: 1 */
|
||||||
step?: number
|
step?: number
|
||||||
/** Range modu (iki handle). Varsayılan: false */
|
/** Range modu (iki handle). Varsayılan: false */
|
||||||
range?: boolean
|
range?: boolean
|
||||||
/** Yön. Varsayılan: 'horizontal' */
|
/** Yön. Varsayılan: 'horizontal' */
|
||||||
orientation?: 'horizontal' | 'vertical'
|
orientation?: 'horizontal' | 'vertical'
|
||||||
/** Track yüksekliği/genişliği (px). Varsayılan: 4 */
|
/** Track yüksekliği/genişliği (px). Varsayılan: 4 */
|
||||||
trackSize?: number
|
trackSize?: number
|
||||||
/** Handle çapı (px). Varsayılan: 18 */
|
/** Handle çapı (px). Varsayılan: 18 */
|
||||||
handleSize?: number
|
handleSize?: number
|
||||||
/** Devre dışı */
|
/** Devre dışı */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Salt okunur */
|
/** Salt okunur */
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
/** Tooltip göster */
|
/** Tooltip göster */
|
||||||
tooltip?: boolean
|
tooltip?: boolean
|
||||||
/** Adım işaretleri göster */
|
/** Adım işaretleri göster */
|
||||||
marks?: boolean | { value: number; label?: string }[]
|
marks?: boolean | { value: number; label?: string }[]
|
||||||
/** Değer değiştiğinde (sürükleme dahil) */
|
/** Değer değiştiğinde (sürükleme dahil) */
|
||||||
onChange?: (value: SliderValue) => void
|
onChange?: (value: SliderValue) => void
|
||||||
/** Sadece bırakıldığında */
|
/** Sadece bırakıldığında */
|
||||||
onAfterChange?: (value: SliderValue) => void
|
onAfterChange?: (value: SliderValue) => void
|
||||||
/** Input adı */
|
/** Input adı */
|
||||||
name?: string
|
name?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
field?: any
|
field?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
|
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
|
||||||
const snap = (v: number, step: number, min: number) =>
|
const snap = (v: number, step: number, min: number) => Math.round((v - min) / step) * step + min
|
||||||
Math.round((v - min) / step) * step + min
|
|
||||||
|
|
||||||
const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
|
const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 100,
|
max = 100,
|
||||||
step = 1,
|
step = 1,
|
||||||
range = false,
|
range = false,
|
||||||
orientation = 'horizontal',
|
orientation = 'horizontal',
|
||||||
trackSize = 4,
|
trackSize = 4,
|
||||||
handleSize = 18,
|
handleSize = 18,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
tooltip = false,
|
tooltip = false,
|
||||||
marks,
|
marks,
|
||||||
onChange,
|
onChange,
|
||||||
onAfterChange,
|
onAfterChange,
|
||||||
name,
|
name,
|
||||||
field,
|
field,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const isVertical = orientation === 'vertical'
|
const isVertical = orientation === 'vertical'
|
||||||
const isControlled = valueProp !== undefined
|
const isControlled = valueProp !== undefined
|
||||||
|
|
||||||
const resolveDefault = (): SliderValue => {
|
const resolveDefault = (): SliderValue => {
|
||||||
if (field?.value !== undefined) return field.value
|
if (field?.value !== undefined) return field.value
|
||||||
if (isControlled) return valueProp!
|
if (isControlled) return valueProp!
|
||||||
if (defaultValue !== undefined) return defaultValue
|
if (defaultValue !== undefined) return defaultValue
|
||||||
return range ? [min, max] : min
|
return range ? [min, max] : min
|
||||||
}
|
}
|
||||||
|
|
||||||
const [internalValue, setInternalValue] = useState<SliderValue>(resolveDefault)
|
const [internalValue, setInternalValue] = useState<SliderValue>(resolveDefault)
|
||||||
const activeHandle = useRef<0 | 1>(0)
|
const activeHandle = useRef<0 | 1>(0)
|
||||||
const trackRef = useRef<HTMLDivElement>(null)
|
const trackRef = useRef<HTMLDivElement>(null)
|
||||||
const dragging = useRef(false)
|
const dragging = useRef(false)
|
||||||
const [showTooltip, setShowTooltip] = useState<boolean[]>([false, false])
|
const [showTooltip, setShowTooltip] = useState<boolean[]>([false, false])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled) setInternalValue(valueProp!)
|
if (isControlled) setInternalValue(valueProp!)
|
||||||
}, [isControlled, valueProp])
|
}, [isControlled, valueProp])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field?.value !== undefined) setInternalValue(field.value)
|
if (field?.value !== undefined) setInternalValue(field.value)
|
||||||
}, [field?.value])
|
}, [field?.value])
|
||||||
|
|
||||||
const { themeColor, primaryColorLevel } = useConfig()
|
const { themeColor, primaryColorLevel } = useConfig()
|
||||||
|
|
||||||
const currentValue = isControlled ? valueProp! : internalValue
|
const currentValue = isControlled ? valueProp! : internalValue
|
||||||
|
|
||||||
const toArray = (v: SliderValue): [number, number] =>
|
const toArray = (v: SliderValue): [number, number] => (Array.isArray(v) ? v : [v, v])
|
||||||
Array.isArray(v) ? v : [v, v]
|
|
||||||
|
|
||||||
const commit = useCallback(
|
const commit = useCallback(
|
||||||
(next: SliderValue, final = false) => {
|
(next: SliderValue, final = false) => {
|
||||||
if (!isControlled) setInternalValue(next)
|
if (!isControlled) setInternalValue(next)
|
||||||
field?.onChange?.(next)
|
field?.onChange?.(next)
|
||||||
onChange?.(next)
|
onChange?.(next)
|
||||||
if (final) onAfterChange?.(next)
|
if (final) onAfterChange?.(next)
|
||||||
},
|
},
|
||||||
[isControlled, field, onChange, onAfterChange],
|
[isControlled, field, onChange, onAfterChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
const percentOf = (v: number) => ((v - min) / (max - min)) * 100
|
const percentOf = (v: number) => ((v - min) / (max - min)) * 100
|
||||||
|
|
||||||
const valueFromPointer = useCallback(
|
const valueFromPointer = useCallback(
|
||||||
(clientX: number, clientY: number): number => {
|
(clientX: number, clientY: number): number => {
|
||||||
const track = trackRef.current
|
const track = trackRef.current
|
||||||
if (!track) return min
|
if (!track) return min
|
||||||
const rect = track.getBoundingClientRect()
|
const rect = track.getBoundingClientRect()
|
||||||
let ratio: number
|
let ratio: number
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
ratio = 1 - (clientY - rect.top) / rect.height
|
ratio = 1 - (clientY - rect.top) / rect.height
|
||||||
} else {
|
} else {
|
||||||
ratio = (clientX - rect.left) / rect.width
|
ratio = (clientX - rect.left) / rect.width
|
||||||
}
|
}
|
||||||
const raw = min + clamp(ratio, 0, 1) * (max - min)
|
const raw = min + clamp(ratio, 0, 1) * (max - min)
|
||||||
return clamp(snap(raw, step, min), min, max)
|
return clamp(snap(raw, step, min), min, max)
|
||||||
},
|
},
|
||||||
[isVertical, min, max, step],
|
[isVertical, min, max, step],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerDown = (
|
const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>, handle: 0 | 1) => {
|
||||||
e: ReactPointerEvent<HTMLDivElement>,
|
if (disabled || readOnly) return
|
||||||
handle: 0 | 1,
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
) => {
|
activeHandle.current = handle
|
||||||
if (disabled || readOnly) return
|
dragging.current = true
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
setShowTooltip((t) => {
|
||||||
activeHandle.current = handle
|
const next = [...t]
|
||||||
dragging.current = true
|
next[handle] = true
|
||||||
setShowTooltip((t) => {
|
return next
|
||||||
const next = [...t]
|
})
|
||||||
next[handle] = true
|
}
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
(e: ReactPointerEvent<HTMLDivElement>) => {
|
(e: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
if (!dragging.current || disabled || readOnly) return
|
if (!dragging.current || disabled || readOnly) return
|
||||||
const newVal = valueFromPointer(e.clientX, e.clientY)
|
const newVal = valueFromPointer(e.clientX, e.clientY)
|
||||||
if (range) {
|
if (range) {
|
||||||
const [a, b] = toArray(currentValue)
|
|
||||||
const next: [number, number] =
|
|
||||||
activeHandle.current === 0
|
|
||||||
? [Math.min(newVal, b), b]
|
|
||||||
: [a, Math.max(newVal, a)]
|
|
||||||
commit(next)
|
|
||||||
} else {
|
|
||||||
commit(newVal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled, readOnly, valueFromPointer, range, currentValue, commit],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handlePointerUp = useCallback(
|
|
||||||
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
||||||
if (!dragging.current) return
|
|
||||||
dragging.current = false
|
|
||||||
const newVal = valueFromPointer(e.clientX, e.clientY)
|
|
||||||
const final = range
|
|
||||||
? (() => {
|
|
||||||
const [a, b] = toArray(currentValue)
|
|
||||||
return activeHandle.current === 0
|
|
||||||
? ([Math.min(newVal, b), b] as [number, number])
|
|
||||||
: ([a, Math.max(newVal, a)] as [number, number])
|
|
||||||
})()
|
|
||||||
: newVal
|
|
||||||
commit(final, true)
|
|
||||||
setShowTooltip([false, false])
|
|
||||||
},
|
|
||||||
[valueFromPointer, range, currentValue, commit],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Track tıklama (handle dışı)
|
|
||||||
const handleTrackClick = useCallback(
|
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
if (disabled || readOnly || dragging.current) return
|
|
||||||
const newVal = valueFromPointer(e.clientX, e.clientY)
|
|
||||||
if (range) {
|
|
||||||
const [a, b] = toArray(currentValue)
|
|
||||||
const distA = Math.abs(newVal - a)
|
|
||||||
const distB = Math.abs(newVal - b)
|
|
||||||
const next: [number, number] =
|
|
||||||
distA <= distB
|
|
||||||
? [clamp(snap(newVal, step, min), min, b), b]
|
|
||||||
: [a, clamp(snap(newVal, step, min), a, max)]
|
|
||||||
commit(next, true)
|
|
||||||
} else {
|
|
||||||
commit(newVal, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled, readOnly, valueFromPointer, range, currentValue, step, min, max, commit],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>, handle: 0 | 1) => {
|
|
||||||
if (disabled || readOnly) return
|
|
||||||
const [a, b] = toArray(currentValue)
|
const [a, b] = toArray(currentValue)
|
||||||
const cur = handle === 0 ? a : b
|
const next: [number, number] =
|
||||||
let next = cur
|
activeHandle.current === 0 ? [Math.min(newVal, b), b] : [a, Math.max(newVal, a)]
|
||||||
switch (e.key) {
|
commit(next)
|
||||||
case 'ArrowRight':
|
} else {
|
||||||
case 'ArrowUp':
|
commit(newVal)
|
||||||
e.preventDefault(); next = clamp(snap(cur + step, step, min), min, max); break
|
}
|
||||||
case 'ArrowLeft':
|
},
|
||||||
case 'ArrowDown':
|
[disabled, readOnly, valueFromPointer, range, currentValue, commit],
|
||||||
e.preventDefault(); next = clamp(snap(cur - step, step, min), min, max); break
|
)
|
||||||
case 'Home':
|
|
||||||
e.preventDefault(); next = min; break
|
const handlePointerUp = useCallback(
|
||||||
case 'End':
|
(e: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault(); next = max; break
|
if (!dragging.current) return
|
||||||
case 'PageUp':
|
dragging.current = false
|
||||||
e.preventDefault(); next = clamp(snap(cur + step * 10, step, min), min, max); break
|
const newVal = valueFromPointer(e.clientX, e.clientY)
|
||||||
case 'PageDown':
|
const final = range
|
||||||
e.preventDefault(); next = clamp(snap(cur - step * 10, step, min), min, max); break
|
? (() => {
|
||||||
default: return
|
const [a, b] = toArray(currentValue)
|
||||||
}
|
return activeHandle.current === 0
|
||||||
if (range) {
|
? ([Math.min(newVal, b), b] as [number, number])
|
||||||
const result: [number, number] =
|
: ([a, Math.max(newVal, a)] as [number, number])
|
||||||
handle === 0 ? [Math.min(next, b), b] : [a, Math.max(next, a)]
|
})()
|
||||||
commit(result, true)
|
: newVal
|
||||||
} else {
|
commit(final, true)
|
||||||
commit(next, true)
|
setShowTooltip([false, false])
|
||||||
}
|
},
|
||||||
|
[valueFromPointer, range, currentValue, commit],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track tıklama (handle dışı)
|
||||||
|
const handleTrackClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (disabled || readOnly || dragging.current) return
|
||||||
|
const newVal = valueFromPointer(e.clientX, e.clientY)
|
||||||
|
if (range) {
|
||||||
|
const [a, b] = toArray(currentValue)
|
||||||
|
const distA = Math.abs(newVal - a)
|
||||||
|
const distB = Math.abs(newVal - b)
|
||||||
|
const next: [number, number] =
|
||||||
|
distA <= distB
|
||||||
|
? [clamp(snap(newVal, step, min), min, b), b]
|
||||||
|
: [a, clamp(snap(newVal, step, min), a, max)]
|
||||||
|
commit(next, true)
|
||||||
|
} else {
|
||||||
|
commit(newVal, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, readOnly, valueFromPointer, range, currentValue, step, min, max, commit],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>, handle: 0 | 1) => {
|
||||||
|
if (disabled || readOnly) return
|
||||||
|
const [a, b] = toArray(currentValue)
|
||||||
|
const cur = handle === 0 ? a : b
|
||||||
|
let next = cur
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
next = clamp(snap(cur + step, step, min), min, max)
|
||||||
|
break
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
next = clamp(snap(cur - step, step, min), min, max)
|
||||||
|
break
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault()
|
||||||
|
next = min
|
||||||
|
break
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault()
|
||||||
|
next = max
|
||||||
|
break
|
||||||
|
case 'PageUp':
|
||||||
|
e.preventDefault()
|
||||||
|
next = clamp(snap(cur + step * 10, step, min), min, max)
|
||||||
|
break
|
||||||
|
case 'PageDown':
|
||||||
|
e.preventDefault()
|
||||||
|
next = clamp(snap(cur - step * 10, step, min), min, max)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if (range) {
|
||||||
// Görsel hesaplamalar
|
const result: [number, number] =
|
||||||
const [v0, v1] = toArray(currentValue)
|
handle === 0 ? [Math.min(next, b), b] : [a, Math.max(next, a)]
|
||||||
const p0 = percentOf(v0)
|
commit(result, true)
|
||||||
const p1 = percentOf(v1)
|
} else {
|
||||||
|
commit(next, true)
|
||||||
const trackFillStyle = isVertical
|
|
||||||
? range
|
|
||||||
? { bottom: `${p0}%`, top: `${100 - p1}%` }
|
|
||||||
: { bottom: 0, top: `${100 - p0}%` }
|
|
||||||
: range
|
|
||||||
? { left: `${p0}%`, right: `${100 - p1}%` }
|
|
||||||
: { left: 0, width: `${p0}%` }
|
|
||||||
|
|
||||||
const handle0Style = isVertical
|
|
||||||
? { bottom: `calc(${p0}% - ${handleSize / 2}px)` }
|
|
||||||
: { left: `calc(${p0}% - ${handleSize / 2}px)` }
|
|
||||||
const handle1Style = isVertical
|
|
||||||
? { bottom: `calc(${p1}% - ${handleSize / 2}px)` }
|
|
||||||
: { left: `calc(${p1}% - ${handleSize / 2}px)` }
|
|
||||||
|
|
||||||
// Marks hesapla
|
|
||||||
const resolvedMarks = marks === true
|
|
||||||
? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({
|
|
||||||
value: min + i * step,
|
|
||||||
label: `${min + i * step}`,
|
|
||||||
}))
|
|
||||||
: Array.isArray(marks)
|
|
||||||
? marks
|
|
||||||
: []
|
|
||||||
|
|
||||||
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
|
|
||||||
|
|
||||||
const renderHandle = (handle: 0 | 1) => {
|
|
||||||
const val = handle === 0 ? v0 : v1
|
|
||||||
const hStyle = handle === 0 ? handle0Style : handle1Style
|
|
||||||
const show = showTooltip[handle]
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={handle}
|
|
||||||
className={classNames(
|
|
||||||
'slider-handle',
|
|
||||||
ringClass,
|
|
||||||
disabled && 'slider-handle-disabled',
|
|
||||||
)}
|
|
||||||
style={{ ...hStyle, width: handleSize, height: handleSize }}
|
|
||||||
role="slider"
|
|
||||||
tabIndex={disabled || readOnly ? -1 : 0}
|
|
||||||
aria-valuemin={handle === 0 ? min : v0}
|
|
||||||
aria-valuemax={handle === 1 ? max : v1}
|
|
||||||
aria-valuenow={val}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
aria-readonly={readOnly}
|
|
||||||
onPointerDown={(e) => handlePointerDown(e, handle)}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, handle)}
|
|
||||||
onFocus={() =>
|
|
||||||
tooltip && setShowTooltip((t) => { const n = [...t]; n[handle] = true; return n })
|
|
||||||
}
|
|
||||||
onBlur={() =>
|
|
||||||
setShowTooltip((t) => { const n = [...t]; n[handle] = false; return n })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(tooltip || show) && show && (
|
|
||||||
<div className="slider-tooltip">{val}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Görsel hesaplamalar
|
||||||
|
const [v0, v1] = toArray(currentValue)
|
||||||
|
const p0 = percentOf(v0)
|
||||||
|
const p1 = percentOf(v1)
|
||||||
|
|
||||||
|
const trackFillStyle = isVertical
|
||||||
|
? range
|
||||||
|
? { bottom: `${p0}%`, top: `${100 - p1}%` }
|
||||||
|
: { bottom: 0, top: `${100 - p0}%` }
|
||||||
|
: range
|
||||||
|
? { left: `${p0}%`, right: `${100 - p1}%` }
|
||||||
|
: { left: 0, width: `${p0}%` }
|
||||||
|
|
||||||
|
const handle0Style = isVertical
|
||||||
|
? { bottom: `calc(${p0}% - ${handleSize / 2}px)` }
|
||||||
|
: { left: `calc(${p0}% - ${handleSize / 2}px)` }
|
||||||
|
const handle1Style = isVertical
|
||||||
|
? { bottom: `calc(${p1}% - ${handleSize / 2}px)` }
|
||||||
|
: { left: `calc(${p1}% - ${handleSize / 2}px)` }
|
||||||
|
|
||||||
|
// Marks hesapla
|
||||||
|
const resolvedMarks =
|
||||||
|
marks === true
|
||||||
|
? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => ({
|
||||||
|
value: min + i * step,
|
||||||
|
label: `${min + i * step}`,
|
||||||
|
}))
|
||||||
|
: Array.isArray(marks)
|
||||||
|
? marks
|
||||||
|
: []
|
||||||
|
|
||||||
|
const ringClass = `focus:ring-${themeColor}-${primaryColorLevel}`
|
||||||
|
|
||||||
|
const renderHandle = (handle: 0 | 1) => {
|
||||||
|
const val = handle === 0 ? v0 : v1
|
||||||
|
const hStyle = handle === 0 ? handle0Style : handle1Style
|
||||||
|
const show = showTooltip[handle]
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
key={handle}
|
||||||
className={classNames(
|
className={classNames('slider-handle', ringClass, disabled && 'slider-handle-disabled')}
|
||||||
'slider',
|
style={{ ...hStyle, width: handleSize, height: handleSize }}
|
||||||
isVertical ? 'slider-vertical' : 'slider-horizontal',
|
role="slider"
|
||||||
disabled && 'slider-disabled',
|
tabIndex={disabled || readOnly ? -1 : 0}
|
||||||
className,
|
aria-valuemin={handle === 0 ? min : v0}
|
||||||
)}
|
aria-valuemax={handle === 1 ? max : v1}
|
||||||
style={style}
|
aria-valuenow={val}
|
||||||
{...rest}
|
aria-disabled={disabled}
|
||||||
>
|
aria-readonly={readOnly}
|
||||||
{name && (
|
onPointerDown={(e) => handlePointerDown(e, handle)}
|
||||||
<input
|
onPointerMove={handlePointerMove}
|
||||||
type="hidden"
|
onPointerUp={handlePointerUp}
|
||||||
name={name ?? field?.name}
|
onKeyDown={(e) => handleKeyDown(e, handle)}
|
||||||
value={Array.isArray(currentValue) ? currentValue.join(',') : currentValue}
|
onFocus={() =>
|
||||||
/>
|
tooltip &&
|
||||||
)}
|
setShowTooltip((t) => {
|
||||||
<div
|
const n = [...t]
|
||||||
ref={trackRef}
|
n[handle] = true
|
||||||
className="slider-track"
|
return n
|
||||||
style={
|
})
|
||||||
isVertical
|
}
|
||||||
? { width: trackSize }
|
onBlur={() =>
|
||||||
: { height: trackSize }
|
setShowTooltip((t) => {
|
||||||
}
|
const n = [...t]
|
||||||
onClick={handleTrackClick}
|
n[handle] = false
|
||||||
>
|
return n
|
||||||
<div className="slider-fill" style={trackFillStyle} />
|
})
|
||||||
|
}
|
||||||
{renderHandle(0)}
|
>
|
||||||
{range && renderHandle(1)}
|
{(tooltip || show) && show && <div className="slider-tooltip">{val}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Marks */}
|
|
||||||
{resolvedMarks.length > 0 && (
|
|
||||||
<div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}>
|
|
||||||
{resolvedMarks.map((m) => {
|
|
||||||
const pct = percentOf(m.value)
|
|
||||||
const markStyle = isVertical
|
|
||||||
? { bottom: `${pct}%` }
|
|
||||||
: { left: `${pct}%` }
|
|
||||||
const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0
|
|
||||||
return (
|
|
||||||
<span key={m.value} className="slider-mark-wrapper" style={markStyle}>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'slider-mark-dot',
|
|
||||||
active && 'slider-mark-dot-active',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{m.label && (
|
|
||||||
<span className="slider-mark-label">{m.label}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
'slider',
|
||||||
|
isVertical ? 'slider-vertical' : 'slider-horizontal',
|
||||||
|
disabled && 'slider-disabled',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{name && (
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name={name ?? field?.name}
|
||||||
|
value={Array.isArray(currentValue) ? currentValue.join(',') : currentValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={trackRef}
|
||||||
|
className="slider-track"
|
||||||
|
style={isVertical ? { width: trackSize } : { height: trackSize }}
|
||||||
|
onClick={handleTrackClick}
|
||||||
|
>
|
||||||
|
<div className="slider-fill" style={trackFillStyle} />
|
||||||
|
|
||||||
|
{renderHandle(0)}
|
||||||
|
{range && renderHandle(1)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Marks */}
|
||||||
|
{resolvedMarks.length > 0 && (
|
||||||
|
<div className={classNames('slider-marks', isVertical && 'slider-marks-vertical')}>
|
||||||
|
{resolvedMarks.map((m) => {
|
||||||
|
const pct = percentOf(m.value)
|
||||||
|
const markStyle = isVertical ? { bottom: `${pct}%` } : { left: `${pct}%` }
|
||||||
|
const active = range ? m.value >= v0 && m.value <= v1 : m.value <= v0
|
||||||
|
return (
|
||||||
|
<span key={m.value} className="slider-mark-wrapper" style={markStyle}>
|
||||||
|
<span
|
||||||
|
className={classNames('slider-mark-dot', active && 'slider-mark-dot-active')}
|
||||||
|
/>
|
||||||
|
{m.label && <span className="slider-mark-label">{m.label}</span>}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Slider.displayName = 'Slider'
|
Slider.displayName = 'Slider'
|
||||||
|
|
|
||||||
|
|
@ -1,220 +1,220 @@
|
||||||
import { NotificationPlacement } from '../@types/placement'
|
import { NotificationPlacement } from '../@types/placement'
|
||||||
|
|
||||||
type GetPlacementTransitionParams = {
|
type GetPlacementTransitionParams = {
|
||||||
offsetX: string | number
|
offsetX: string | number
|
||||||
offsetY: string | number
|
offsetY: string | number
|
||||||
placement: NotificationPlacement
|
placement: NotificationPlacement
|
||||||
transitionType: 'scale' | 'fade'
|
transitionType: 'scale' | 'fade'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Motion = {
|
type Motion = {
|
||||||
opacity: number
|
opacity: number
|
||||||
transform?: string
|
transform?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MotionProps = {
|
type MotionProps = {
|
||||||
initial: Motion
|
initial: Motion
|
||||||
animate: Motion
|
animate: Motion
|
||||||
exit: Motion
|
exit: Motion
|
||||||
}
|
}
|
||||||
|
|
||||||
type MotionDefault = {
|
type MotionDefault = {
|
||||||
top?: string | number
|
top?: string | number
|
||||||
left?: string | number
|
left?: string | number
|
||||||
right?: string | number
|
right?: string | number
|
||||||
bottom?: string | number
|
bottom?: string | number
|
||||||
transform?: string
|
transform?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MotionTransition = {
|
type MotionTransition = {
|
||||||
default: MotionDefault
|
default: MotionDefault
|
||||||
variants: MotionProps
|
variants: MotionProps
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPlacementTransition = ({
|
export const getPlacementTransition = ({
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
placement,
|
placement,
|
||||||
transitionType,
|
transitionType,
|
||||||
}: GetPlacementTransitionParams) => {
|
}: GetPlacementTransitionParams) => {
|
||||||
if (transitionType === 'fade') {
|
if (transitionType === 'fade') {
|
||||||
return fadeTransition(offsetX, offsetY)[placement]
|
return fadeTransition(offsetX, offsetY)[placement]
|
||||||
}
|
}
|
||||||
|
|
||||||
return scaleTransition(offsetX, offsetY)[placement]
|
return scaleTransition(offsetX, offsetY)[placement]
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleMotionProps = {
|
const scaleMotionProps = {
|
||||||
initial: {
|
initial: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: 'scale(0.75)',
|
transform: 'scale(0.75)',
|
||||||
},
|
},
|
||||||
animate: {
|
animate: {
|
||||||
transform: 'scale(1)',
|
transform: 'scale(1)',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
exit: {
|
exit: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: 'scale(0.75)',
|
transform: 'scale(0.75)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const fadeMotionProps = {
|
const fadeMotionProps = {
|
||||||
initial: {
|
initial: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
animate: {
|
animate: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
exit: {
|
exit: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleTransition = (
|
const scaleTransition = (
|
||||||
offsetX: number | string,
|
offsetX: number | string,
|
||||||
offsetY: number | string
|
offsetY: number | string,
|
||||||
): Record<NotificationPlacement, MotionTransition> => {
|
): Record<NotificationPlacement, MotionTransition> => {
|
||||||
return {
|
return {
|
||||||
'top-end': {
|
'top-end': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
right: offsetX,
|
right: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'top-start': {
|
'top-start': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
left: offsetX,
|
left: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'top-center': {
|
'top-center': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'middle-center': {
|
'middle-center': {
|
||||||
default: {
|
default: {
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-end': {
|
'bottom-end': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
right: offsetX,
|
right: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-start': {
|
'bottom-start': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
left: offsetX,
|
left: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-center': {
|
'bottom-center': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...scaleMotionProps,
|
...scaleMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fadeTransition = (
|
const fadeTransition = (
|
||||||
offsetX: number | string,
|
offsetX: number | string,
|
||||||
offsetY: number | string
|
offsetY: number | string,
|
||||||
): Record<NotificationPlacement, MotionTransition> => {
|
): Record<NotificationPlacement, MotionTransition> => {
|
||||||
return {
|
return {
|
||||||
'top-end': {
|
'top-end': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
right: offsetX,
|
right: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'top-start': {
|
'top-start': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
left: offsetX,
|
left: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'top-center': {
|
'top-center': {
|
||||||
default: {
|
default: {
|
||||||
top: offsetY,
|
top: offsetY,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'middle-center': {
|
'middle-center': {
|
||||||
default: {
|
default: {
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-end': {
|
'bottom-end': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
right: offsetX,
|
right: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-start': {
|
'bottom-start': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
left: offsetX,
|
left: offsetX,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bottom-center': {
|
'bottom-center': {
|
||||||
default: {
|
default: {
|
||||||
bottom: offsetY,
|
bottom: offsetY,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
...fadeMotionProps,
|
...fadeMotionProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import ScriptBuilderDialog from '@/components/scriptBuilder/ScriptBuilderDialog'
|
import ScriptBuilderDialog from '@/components/scriptBuilder/ScriptBuilderDialog'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { createDesignerScriptDialect } from './designerScriptDialect'
|
import { createDesignerScriptDialect } from './designerScriptDialect'
|
||||||
import type { SqlDataSourceEvent } from './types'
|
import type { SqlDataSourceEvent } from './types'
|
||||||
|
|
||||||
|
|
@ -37,6 +38,8 @@ function DesignerScriptBuilderDialog({
|
||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
}: DesignerScriptBuilderDialogProps) {
|
}: DesignerScriptBuilderDialogProps) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
// Çağıran her render'da yeni diziler üretiyor; lehçe içeriklerine göre
|
// Çağıran her render'da yeni diziler üretiyor; lehçe içeriklerine göre
|
||||||
// sabitlenmezse editör yazarken sıfırlanırdı.
|
// sabitlenmezse editör yazarken sıfırlanırdı.
|
||||||
const recordFieldKey = recordFields.join('|')
|
const recordFieldKey = recordFields.join('|')
|
||||||
|
|
@ -50,8 +53,9 @@ function DesignerScriptBuilderDialog({
|
||||||
recordFields: recordFieldKey ? recordFieldKey.split('|') : [],
|
recordFields: recordFieldKey ? recordFieldKey.split('|') : [],
|
||||||
refNames: refNameKey ? refNameKey.split('|') : [],
|
refNames: refNameKey ? refNameKey.split('|') : [],
|
||||||
sqlRef,
|
sqlRef,
|
||||||
|
translate,
|
||||||
}),
|
}),
|
||||||
[componentLabel, eventInfo, eventName, recordFieldKey, refNameKey, sqlRef],
|
[componentLabel, eventInfo, eventName, recordFieldKey, refNameKey, sqlRef, translate],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -187,93 +187,93 @@ export interface DesignerScriptRecipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DESIGNER_SCRIPT_RECIPE_GROUPS = [
|
export const DESIGNER_SCRIPT_RECIPE_GROUPS = [
|
||||||
'Komponent erişimi',
|
'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
'API çağrıları',
|
'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
'SqlDataSource',
|
'App.VisualDesigner.Catalog.Group.SqlDataSource',
|
||||||
'Event ve sayfa',
|
'App.VisualDesigner.Catalog.Group.EventAndPage',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const DESIGNER_SCRIPT_RECIPES: DesignerScriptRecipe[] = [
|
export const DESIGNER_SCRIPT_RECIPES: DesignerScriptRecipe[] = [
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Değer atama ve okuma',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title',
|
||||||
description: 'Her komponent kendi ref adıyla adreslenir.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description',
|
||||||
code: `refs.{{ref}}.setValue('Merhaba')
|
code: `refs.{{ref}}.setValue('Merhaba')
|
||||||
const value = refs.{{ref}}.getValue()
|
const value = refs.{{ref}}.getValue()
|
||||||
console.log(value)`,
|
console.log(value)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Aktif / pasif yapma',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title2',
|
||||||
description: 'Kaydet butonunu koşula göre kilitlemek için.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description2',
|
||||||
code: `refs.{{ref}}.setEnabled(false)
|
code: `refs.{{ref}}.setEnabled(false)
|
||||||
console.log(refs.{{ref}}.isEnabled())`,
|
console.log(refs.{{ref}}.isEnabled())`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Gösterme / gizleme',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title3',
|
||||||
description: 'Gizlenen komponent sayfada hiç render edilmez.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description3',
|
||||||
code: `refs.{{ref}}.setVisible(false)
|
code: `refs.{{ref}}.setVisible(false)
|
||||||
// veya: refs.{{ref}}.hide() / refs.{{ref}}.show()`,
|
// veya: refs.{{ref}}.hide() / refs.{{ref}}.show()`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Salt okunur yapma',
|
title: 'Salt okunur yapma',
|
||||||
description: 'Değeri görünür kalsın ama düzenlenemesin.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description4',
|
||||||
code: `refs.{{ref}}.setReadOnly(true)`,
|
code: `refs.{{ref}}.setReadOnly(true)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Etiket / metin değiştirme',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title4',
|
||||||
description: 'Buton ve etiket yazısını çalışma anında değiştirir.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description5',
|
||||||
code: `refs.{{ref}}.setText('Kaydediliyor…')`,
|
code: `refs.{{ref}}.setText('Kaydediliyor…')`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Serbest property atama',
|
title: 'Serbest property atama',
|
||||||
description: 'Property panelindeki her property script ile de yazılabilir.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description6',
|
||||||
code: `refs.{{ref}}.setProps({ placeholder: 'Ara…', className: 'border-red-500' })
|
code: `refs.{{ref}}.setProps({ placeholder: 'Ara…', className: 'border-red-500' })
|
||||||
refs.{{ref}}.setProp('size', 'sm')`,
|
refs.{{ref}}.setProp('size', 'sm')`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Script ile yapılanları geri alma',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title5',
|
||||||
description: 'Komponenti tasarımdaki haline döndürür.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description7',
|
||||||
code: `refs.{{ref}}.reset()`,
|
code: `refs.{{ref}}.reset()`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'GET — kayıt okuma',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title',
|
||||||
description: 'Sonuç doğrudan response body’sidir.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description',
|
||||||
code: `const rows = await api.get('{{url}}')
|
code: `const rows = await api.get('{{url}}')
|
||||||
console.log(rows)`,
|
console.log(rows)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'GET — parametre ile',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title2',
|
||||||
description: 'İkinci argüman query string olarak gönderilir.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description2',
|
||||||
code: `const rows = await api.get('{{url}}', { maxResultCount: 10 })
|
code: `const rows = await api.get('{{url}}', { maxResultCount: 10 })
|
||||||
console.log(rows)`,
|
console.log(rows)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'POST — yeni kayıt',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title3',
|
||||||
description: 'Gövde ikinci argüman olarak verilir.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description3',
|
||||||
code: `const created = await api.post('{{url}}', {
|
code: `const created = await api.post('{{url}}', {
|
||||||
name: refs.{{ref}}.getValue(),
|
name: refs.{{ref}}.getValue(),
|
||||||
})
|
})
|
||||||
console.log(created)`,
|
console.log(created)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'PUT ve DELETE',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title4',
|
||||||
description: 'Güncelleme ve silme aynı imzayı kullanır.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description4',
|
||||||
code: `await api.put('{{url}}/5', { name: 'Yeni ad' })
|
code: `await api.put('{{url}}/5', { name: 'Yeni ad' })
|
||||||
await api.delete('{{url}}/5')`,
|
await api.delete('{{url}}/5')`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'Hata yönetimi ve bildirim',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title5',
|
||||||
description: 'notify tipleri: success, info, warning, danger.',
|
description: 'notify tipleri: success, info, warning, danger.',
|
||||||
code: `try {
|
code: `try {
|
||||||
await api.post('{{url}}', { name: refs.{{ref}}.getValue() })
|
await api.post('{{url}}', { name: refs.{{ref}}.getValue() })
|
||||||
|
|
@ -283,9 +283,9 @@ await api.delete('{{url}}/5')`,
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'Çağrı boyunca butonu kilitleme',
|
title: 'App.VisualDesigner.Catalog.ApiCalls.Title6',
|
||||||
description: 'Çift tıklamayı ve mükerrer isteği önler.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description5',
|
||||||
code: `refs.{{ref}}.setEnabled(false)
|
code: `refs.{{ref}}.setEnabled(false)
|
||||||
refs.{{ref}}.setText('Gönderiliyor…')
|
refs.{{ref}}.setText('Gönderiliyor…')
|
||||||
try {
|
try {
|
||||||
|
|
@ -299,9 +299,9 @@ try {
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'API çağrıları',
|
group: 'App.VisualDesigner.Catalog.Group.ApiCalls',
|
||||||
title: 'Serbest istek',
|
title: 'Serbest istek',
|
||||||
description: 'Header, timeout gibi ayarlar gerektiğinde.',
|
description: 'App.VisualDesigner.Catalog.ApiCalls.Description6',
|
||||||
code: `const result = await api.request({
|
code: `const result = await api.request({
|
||||||
url: '{{url}}',
|
url: '{{url}}',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -310,26 +310,25 @@ try {
|
||||||
console.log(result)`,
|
console.log(result)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Select’in görünmeyen sütunlarını okuma',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title6',
|
||||||
description:
|
description: 'App.VisualDesigner.Catalog.SelectHiddenColumns.Description',
|
||||||
'Select yalnızca Label gösterir; Data sekmesinde eklenen sütunlar seçenekle birlikte taşınır.',
|
|
||||||
code: `// onChange içinde: seçilen kaydın diğer sütunları
|
code: `// onChange içinde: seçilen kaydın diğer sütunları
|
||||||
const fiyat = refs.{{ref}}.getColumn('unitPrice')
|
const fiyat = refs.{{ref}}.getColumn('unitPrice')
|
||||||
const stok = refs.{{ref}}.getColumn('stockQuantity')
|
const stok = refs.{{ref}}.getColumn('stockQuantity')
|
||||||
refs.{{sql}}.setFields({ UnitPrice: fiyat, Stock: stok })`,
|
refs.{{sql}}.setFields({ UnitPrice: fiyat, Stock: stok })`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Komponent erişimi',
|
group: 'App.VisualDesigner.Catalog.Group.ComponentAccess',
|
||||||
title: 'Seçilen kaydın tamamı',
|
title: 'App.VisualDesigner.Catalog.ComponentAccess.Title7',
|
||||||
description: 'Çoklu seçimde dizi döner.',
|
description: 'App.VisualDesigner.Catalog.ComponentAccess.Description8',
|
||||||
code: `const secilen = refs.{{ref}}.getSelectedOption()
|
code: `const secilen = refs.{{ref}}.getSelectedOption()
|
||||||
console.log(secilen, refs.{{ref}}.getLabel())`,
|
console.log(secilen, refs.{{ref}}.getLabel())`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'SqlDataSource',
|
group: 'SqlDataSource',
|
||||||
title: 'Kaydet / sil / yenile',
|
title: 'App.VisualDesigner.Catalog.SqlDataSource.Title',
|
||||||
description: 'Toolbar’ı gizleyip kendi butonlarınızı kullanabilirsiniz.',
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description',
|
||||||
code: `await refs.{{sql}}.save()
|
code: `await refs.{{sql}}.save()
|
||||||
// await refs.{{sql}}.remove()
|
// await refs.{{sql}}.remove()
|
||||||
// await refs.{{sql}}.reload()
|
// await refs.{{sql}}.reload()
|
||||||
|
|
@ -337,23 +336,23 @@ console.log(secilen, refs.{{ref}}.getLabel())`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'SqlDataSource',
|
group: 'SqlDataSource',
|
||||||
title: 'Aktif kaydı okuma ve yazma',
|
title: 'App.VisualDesigner.Catalog.SqlDataSource.Title2',
|
||||||
description: 'Alan adı Select sonucundaki sütun adıdır.',
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description2',
|
||||||
code: `const record = refs.{{sql}}.getRecord()
|
code: `const record = refs.{{sql}}.getRecord()
|
||||||
console.log(record)
|
console.log(record)
|
||||||
refs.{{sql}}.setField('name', 'Yeni ad')`,
|
refs.{{sql}}.setField('name', 'Yeni ad')`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'SqlDataSource',
|
group: 'SqlDataSource',
|
||||||
title: 'Kayıtlar arasında gezinme',
|
title: 'App.VisualDesigner.Catalog.SqlDataSource.Title3',
|
||||||
description: 'Select birden fazla satır döndürdüğünde.',
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description3',
|
||||||
code: `const rows = refs.{{sql}}.getRows()
|
code: `const rows = refs.{{sql}}.getRows()
|
||||||
refs.{{sql}}.goToRow(rows.length - 1)`,
|
refs.{{sql}}.goToRow(rows.length - 1)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'SqlDataSource',
|
group: 'SqlDataSource',
|
||||||
title: 'Kaydetmeden önce doğrulama',
|
title: 'App.VisualDesigner.Catalog.SqlDataSource.Title4',
|
||||||
description: 'Zorunlu alan boşsa işlemi durdurur.',
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description4',
|
||||||
code: `const record = refs.{{sql}}.getRecord()
|
code: `const record = refs.{{sql}}.getRecord()
|
||||||
if (!record.name) {
|
if (!record.name) {
|
||||||
notify('Ad alanı zorunludur', 'warning')
|
notify('Ad alanı zorunludur', 'warning')
|
||||||
|
|
@ -363,35 +362,35 @@ await refs.{{sql}}.save()`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'SqlDataSource',
|
group: 'SqlDataSource',
|
||||||
title: 'Durum bilgisi',
|
title: 'App.VisualDesigner.Catalog.SqlDataSource.Title5',
|
||||||
description: 'Yeni kayıt / düzenleme, meşgul ve hata durumu.',
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description5',
|
||||||
code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`,
|
code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Event ve sayfa',
|
group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
|
||||||
title: 'Event verisi',
|
title: 'Event verisi',
|
||||||
description: 'event içeriği komponent tipine göre değişir.',
|
description: 'App.VisualDesigner.Catalog.EventAndPage.Description',
|
||||||
code: `// Input: event.target.value · Checkbox: event.checked
|
code: `// Input: event.target.value · Checkbox: event.checked
|
||||||
// Select/Dropdown/Menu: seçilen değer · Button: tıklama eventi
|
// Select/Dropdown/Menu: seçilen değer · Button: tıklama eventi
|
||||||
console.log(event)`,
|
console.log(event)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Event ve sayfa',
|
group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
|
||||||
title: 'Komponentin kendi bilgileri',
|
title: 'Komponentin kendi bilgileri',
|
||||||
description: 'Scriptin bağlı olduğu komponent ve tasarım property’leri.',
|
description: 'App.VisualDesigner.Catalog.EventAndPage.Description2',
|
||||||
code: `console.log(component.type, component.ref)
|
code: `console.log(component.type, component.ref)
|
||||||
console.log(props)`,
|
console.log(props)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Event ve sayfa',
|
group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
|
||||||
title: 'Sayfa yönlendirme',
|
title: 'App.VisualDesigner.Catalog.EventAndPage.Title',
|
||||||
description: 'Kayıt sonrası liste sayfasına dönmek için.',
|
description: 'App.VisualDesigner.Catalog.EventAndPage.Description3',
|
||||||
code: `window.location.href = '/app/orders'`,
|
code: `window.location.href = '/app/orders'`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Event ve sayfa',
|
group: 'App.VisualDesigner.Catalog.Group.EventAndPage',
|
||||||
title: 'URL parametresi okuma',
|
title: 'URL parametresi okuma',
|
||||||
description: 'Detay sayfasında anahtar değerini almak için.',
|
description: 'App.VisualDesigner.Catalog.EventAndPage.Description4',
|
||||||
code: `const id = new URLSearchParams(window.location.search).get('id')
|
code: `const id = new URLSearchParams(window.location.search).get('id')
|
||||||
console.log(id)`,
|
console.log(id)`,
|
||||||
},
|
},
|
||||||
|
|
@ -405,7 +404,7 @@ const COLLECTION_ITEMS_PROPERTY = {
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
value: [],
|
value: [],
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Statik veya endpoint kaynaklı veri koleksiyonu',
|
description: 'App.VisualDesigner.Catalog.Items.Description',
|
||||||
tsType: 'unknown[]',
|
tsType: 'unknown[]',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -415,7 +414,7 @@ const GRID_COMPACT_PROPERTY = {
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
value: false,
|
value: false,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Satır ve hücre boşluklarını azaltır',
|
description: 'App.VisualDesigner.Catalog.Compact.Description',
|
||||||
tsType: 'boolean',
|
tsType: 'boolean',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -426,7 +425,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
value: true,
|
value: true,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Satırların üzerine gelindiğinde vurgu rengi gösterir',
|
description: 'App.VisualDesigner.Catalog.Hoverable.Description',
|
||||||
tsType: 'boolean',
|
tsType: 'boolean',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -436,7 +435,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
value: true,
|
value: true,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Geniş tablolarda yatay kaydırmayı etkinleştirir',
|
description: 'App.VisualDesigner.Catalog.Overflow.Description',
|
||||||
tsType: 'boolean',
|
tsType: 'boolean',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -446,7 +445,7 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
value: false,
|
value: false,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Veri satırları arasındaki kenarlıkları kaldırır',
|
description: 'App.VisualDesigner.Catalog.BorderlessRow.Description',
|
||||||
tsType: 'boolean',
|
tsType: 'boolean',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -458,7 +457,7 @@ const TABLE_DIMENSION_PROPERTIES = [
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
value: 2,
|
value: 2,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Table içindeki statik satır sayısı',
|
description: 'App.VisualDesigner.Catalog.RowCount.Description',
|
||||||
tsType: 'number',
|
tsType: 'number',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -468,7 +467,7 @@ const TABLE_DIMENSION_PROPERTIES = [
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
value: 3,
|
value: 3,
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Her satırdaki statik hücre sayısı',
|
description: 'App.VisualDesigner.Catalog.ColumnCount.Description',
|
||||||
tsType: 'number',
|
tsType: 'number',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -478,11 +477,11 @@ const SELECT_OPTIONS_PROPERTY = {
|
||||||
name: 'options',
|
name: 'options',
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
value: [
|
value: [
|
||||||
{ label: 'Seçenek 1', value: '1' },
|
{ label: 'App.VisualDesigner.Catalog.Options.Label', value: '1' },
|
||||||
{ label: 'Seçenek 2', value: '2' },
|
{ label: 'App.VisualDesigner.Catalog.Options.Label2', value: '2' },
|
||||||
],
|
],
|
||||||
category: 'properties' as const,
|
category: 'properties' as const,
|
||||||
description: 'Select items; statik olarak veya endpoint koleksiyonundan beslenebilir',
|
description: 'App.VisualDesigner.Catalog.Options.Description',
|
||||||
tsType: 'Array<{ label: string; value: unknown }>',
|
tsType: 'Array<{ label: string; value: unknown }>',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -490,7 +489,7 @@ const SELECT_OPTIONS_PROPERTY = {
|
||||||
const MENU_ITEMS_PROPERTY = {
|
const MENU_ITEMS_PROPERTY = {
|
||||||
...SELECT_OPTIONS_PROPERTY,
|
...SELECT_OPTIONS_PROPERTY,
|
||||||
name: 'items',
|
name: 'items',
|
||||||
description: 'Menu items; statik olarak veya endpoint koleksiyonundan beslenebilir',
|
description: 'App.VisualDesigner.Catalog.Items.Description2',
|
||||||
}
|
}
|
||||||
const TABS_ITEMS_PROPERTY = {
|
const TABS_ITEMS_PROPERTY = {
|
||||||
...SELECT_OPTIONS_PROPERTY,
|
...SELECT_OPTIONS_PROPERTY,
|
||||||
|
|
@ -499,8 +498,7 @@ const TABS_ITEMS_PROPERTY = {
|
||||||
{ label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' },
|
{ label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' },
|
||||||
{ label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' },
|
{ label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' },
|
||||||
],
|
],
|
||||||
description:
|
description: 'App.VisualDesigner.Catalog.Tabs.Description',
|
||||||
'Sekmeler; statik olarak veya endpoint koleksiyonundan beslenebilir. content alanı sekme içeriğini yazar.',
|
|
||||||
tsType: 'Array<{ label: string; value: unknown; content?: string }>',
|
tsType: 'Array<{ label: string; value: unknown; content?: string }>',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,19 +721,24 @@ const platformDefinition = (
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [
|
export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [
|
||||||
|
platformDefinition('ListView', 'List', 'App.VisualDesigner.Catalog.ListView.Description'),
|
||||||
|
platformDefinition('DataGridView', 'Grid', 'App.VisualDesigner.Catalog.DataGridView.Description'),
|
||||||
|
platformDefinition('TreeView', 'Tree', 'App.VisualDesigner.Catalog.TreeView.Description'),
|
||||||
|
platformDefinition('GanttView', 'GanttView', 'App.VisualDesigner.Catalog.GanttView.Description'),
|
||||||
|
platformDefinition('TodoBoard', 'TodoBoard', 'App.VisualDesigner.Catalog.TodoBoard.Description'),
|
||||||
|
platformDefinition('CardView', 'CardView', 'App.VisualDesigner.Catalog.CardView.Description'),
|
||||||
platformDefinition(
|
platformDefinition(
|
||||||
'ListView',
|
'SchedulerView',
|
||||||
'List',
|
'SchedulerView',
|
||||||
'Tüm etkin platform görünümlerini /admin/list/:listFormCode ile aynı şekilde yönetir',
|
'App.VisualDesigner.Catalog.SchedulerView.Description',
|
||||||
|
),
|
||||||
|
platformDefinition('PivotView', 'Pivot', 'App.VisualDesigner.Catalog.PivotView.Description'),
|
||||||
|
platformDefinition(
|
||||||
|
'ChartView',
|
||||||
|
'Chart',
|
||||||
|
'App.VisualDesigner.Catalog.ChartView.Description',
|
||||||
|
'420px',
|
||||||
),
|
),
|
||||||
platformDefinition('DataGridView', 'Grid', 'Sozsoft dinamik veri grid görünümü'),
|
|
||||||
platformDefinition('TreeView', 'Tree', 'Sozsoft dinamik ağaç görünümü'),
|
|
||||||
platformDefinition('GanttView', 'GanttView', 'Sozsoft dinamik Gantt görünümü'),
|
|
||||||
platformDefinition('TodoBoard', 'TodoBoard', 'Sozsoft dinamik iş panosu'),
|
|
||||||
platformDefinition('CardView', 'CardView', 'Sozsoft dinamik kart görünümü'),
|
|
||||||
platformDefinition('SchedulerView', 'SchedulerView', 'Sozsoft dinamik planlayıcı görünümü'),
|
|
||||||
platformDefinition('PivotView', 'Pivot', 'Sozsoft dinamik pivot görünümü'),
|
|
||||||
platformDefinition('ChartView', 'Chart', 'Sozsoft dinamik grafik görünümü', '420px'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -750,8 +753,7 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
|
||||||
category: 'data',
|
category: 'data',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'data',
|
toolboxGroup: 'data',
|
||||||
description:
|
description: 'App.VisualDesigner.Catalog.SqlDataSource.Description',
|
||||||
'GET/POST/PUT/DELETE endpointlerini tek kayıt üzerinde yöneten veri kabı; içine bırakılan komponentler sütunlara bağlanır',
|
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|
@ -759,42 +761,42 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Kaydı okuyan GET endpointinin data source id’si (Data sekmesinden seçilir)',
|
description: 'App.VisualDesigner.Catalog.String.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'insertEndpoint',
|
name: 'insertEndpoint',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Yeni kayıt için POST endpointi',
|
description: 'App.VisualDesigner.Catalog.String.Description2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'updateEndpoint',
|
name: 'updateEndpoint',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Mevcut kaydı güncelleyen PUT endpointi',
|
description: 'App.VisualDesigner.Catalog.String.Description3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'deleteEndpoint',
|
name: 'deleteEndpoint',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Kaydı silen DELETE endpointi',
|
description: 'App.VisualDesigner.Catalog.String.Description4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'keyFieldName',
|
name: 'keyFieldName',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Insert/Update ayrımı ve endpoint parametreleri bu sütundan doldurulur',
|
description: 'App.VisualDesigner.Catalog.String.Description5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'collectionPath',
|
name: 'collectionPath',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'GET cevabı içinde satırların bulunduğu path (boşsa cevabın kendisi kullanılır)',
|
description: 'App.VisualDesigner.Catalog.String.Description6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'keySource',
|
name: 'keySource',
|
||||||
|
|
@ -802,36 +804,35 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
|
||||||
value: 'query',
|
value: 'query',
|
||||||
options: ['query', 'route'],
|
options: ['query', 'route'],
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Key değerinin sayfa URL’sinden okunma şekli',
|
description: 'App.VisualDesigner.Catalog.Select.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'keyParamName',
|
name: 'keyParamName',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'URL’den okunacak parametre adı (boşsa key field kullanılır)',
|
description: 'App.VisualDesigner.Catalog.String.Description7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'previewKeyValue',
|
name: 'previewKeyValue',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description:
|
description: 'App.VisualDesigner.Catalog.PreviewKeyValue.Description',
|
||||||
'Sadece tasarım ekranı: GetById endpointinin sütunlarını okuyabilmek için örnek key değeri',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'autoLoad',
|
name: 'autoLoad',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
value: true,
|
value: true,
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'Sayfa açıldığında Select endpointini otomatik çağırır',
|
description: 'App.VisualDesigner.Catalog.Boolean.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showToolbar',
|
name: 'showToolbar',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
value: true,
|
value: true,
|
||||||
category: 'properties',
|
category: 'properties',
|
||||||
description: 'New / Save / Delete / Reload butonlarını gösterir',
|
description: 'App.VisualDesigner.Catalog.Boolean.Description2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gap',
|
name: 'gap',
|
||||||
|
|
@ -861,7 +862,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'layout',
|
||||||
description: 'Intranet içeriği için ortalanmış sayfa alanı',
|
description: 'App.VisualDesigner.Catalog.PageContainer.Description',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{ name: 'maxWidth', type: 'string', value: '1280px', category: 'styling' },
|
{ name: 'maxWidth', type: 'string', value: '1280px', category: 'styling' },
|
||||||
|
|
@ -877,7 +878,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'layout',
|
||||||
description: 'Sayfayı iki eşit kolona böler',
|
description: 'App.VisualDesigner.Catalog.TwoColumns.Description',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
||||||
|
|
@ -891,7 +892,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'layout',
|
||||||
description: 'Dar sidebar ve geniş içerik alanı',
|
description: 'App.VisualDesigner.Catalog.SidebarContent.Description',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{ name: 'sidebarWidth', type: 'string', value: '280px', category: 'styling' },
|
{ name: 'sidebarWidth', type: 'string', value: '280px', category: 'styling' },
|
||||||
|
|
@ -906,7 +907,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'layout',
|
||||||
description: 'Üst alan ve alt içerik için dikey yerleşim',
|
description: 'App.VisualDesigner.Catalog.HeaderContent.Description',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
||||||
|
|
@ -920,7 +921,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'layout',
|
||||||
description: 'Yatay ve responsive yerleşim',
|
description: 'App.VisualDesigner.Catalog.FlexRow.Description',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
||||||
|
|
@ -942,7 +943,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'ui',
|
toolboxGroup: 'ui',
|
||||||
description: 'Bileşenler arasında boşluk',
|
description: 'App.VisualDesigner.Catalog.Spacer.Description',
|
||||||
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
|
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
|
||||||
hooks: [],
|
hooks: [],
|
||||||
},
|
},
|
||||||
|
|
@ -987,7 +988,7 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
|
||||||
category: 'custom',
|
category: 'custom',
|
||||||
kind: 'custom',
|
kind: 'custom',
|
||||||
toolboxGroup: 'custom',
|
toolboxGroup: 'custom',
|
||||||
description: 'Veritabanındaki yeniden kullanılabilir bileşen',
|
description: 'App.VisualDesigner.Catalog.Spacer.Description2',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
|
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
|
||||||
hooks: [],
|
hooks: [],
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
type DesignerRuleCondition,
|
type DesignerRuleCondition,
|
||||||
type DesignerScriptContext,
|
type DesignerScriptContext,
|
||||||
type DesignerScriptRule,
|
type DesignerScriptRule,
|
||||||
|
type TranslateFn,
|
||||||
} from './designerScriptRecipes'
|
} from './designerScriptRecipes'
|
||||||
import type { SqlDataSourceEvent } from './types'
|
import type { SqlDataSourceEvent } from './types'
|
||||||
|
|
||||||
|
|
@ -75,57 +76,57 @@ const snippets = (sqlRef: string): ScriptSnippet[] => {
|
||||||
const sql = sqlRef || 'sqlDataSource1'
|
const sql = sqlRef || 'sqlDataSource1'
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Alan değişimine göre hesapla',
|
label: 'App.VisualDesigner.Snippet.CalcOnFieldChange.Label',
|
||||||
group: 'Kayıt',
|
group: 'App.VisualDesigner.Snippet.Group.Record',
|
||||||
description: 'Sadece ilgili sütunlar değişince hesaplar.',
|
description: 'App.VisualDesigner.Snippet.CalcOnFieldChange.Description',
|
||||||
code: `if (['\${1:Quantity}', '\${2:UnitPrice}'].includes(event.field)) {\n const total = Number(event.record.\${1:Quantity} || 0) * Number(event.record.\${2:UnitPrice} || 0)\n refs.${sql}.setField('\${3:TotalAmount}', Math.round(total * 100) / 100)\n}`,
|
code: `if (['\${1:Quantity}', '\${2:UnitPrice}'].includes(event.field)) {\n const total = Number(event.record.\${1:Quantity} || 0) * Number(event.record.\${2:UnitPrice} || 0)\n refs.${sql}.setField('\${3:TotalAmount}', Math.round(total * 100) / 100)\n}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kaydetmeden önce doğrula',
|
label: 'App.VisualDesigner.Snippet.ValidateBeforeSave.Label',
|
||||||
group: 'Akış',
|
group: 'App.VisualDesigner.Snippet.Group.Flow',
|
||||||
description: 'Eksik alan varsa işlemi iptal eder.',
|
description: 'App.VisualDesigner.Snippet.ValidateBeforeSave.Description',
|
||||||
code: `const eksik = ['\${1:Name}', '\${2:Code}'].filter((column) => !event.record[column])\nif (eksik.length) {\n notify('Zorunlu alanlar: ' + eksik.join(', '), 'warning')\n return false\n}`,
|
code: `const eksik = ['\${1:Name}', '\${2:Code}'].filter((column) => !event.record[column])\nif (eksik.length) {\n notify('Zorunlu alanlar: ' + eksik.join(', '), 'warning')\n return false\n}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Çok koşullu blok',
|
label: 'App.VisualDesigner.Snippet.MultiCondition.Label',
|
||||||
group: 'Akış',
|
group: 'App.VisualDesigner.Snippet.Group.Flow',
|
||||||
description: 'VE / VEYA ile birleşen koşullar.',
|
description: 'App.VisualDesigner.Snippet.MultiCondition.Description',
|
||||||
code: `if (refs.${sql}.getMode() === 'new' || Number(refs.${sql}.getField('\${1:Total}') || 0) > \${2:1000}) {\n \${3:notify('Kontrol edin')}\n}`,
|
code: `if (refs.${sql}.getMode() === 'new' || Number(refs.${sql}.getField('\${1:Total}') || 0) > \${2:1000}) {\n \${3:notify('Kontrol edin')}\n}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Toplu alan yazma',
|
label: 'App.VisualDesigner.Snippet.BulkSet.Label',
|
||||||
group: 'Kayıt',
|
group: 'App.VisualDesigner.Snippet.Group.Record',
|
||||||
description: 'Birden fazla sütunu tek çağrıda doldurur.',
|
description: 'App.VisualDesigner.Snippet.BulkSet.Description',
|
||||||
code: `refs.${sql}.setFields({ \${1:Status}: '\${2:Taslak}', \${3:Currency}: '\${4:TRY}' })`,
|
code: `refs.${sql}.setFields({ \${1:Status}: '\${2:Taslak}', \${3:Currency}: '\${4:TRY}' })`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Komponent görünürlüğü',
|
label: 'App.VisualDesigner.Snippet.Visibility.Label',
|
||||||
group: 'Komponent',
|
group: 'App.VisualDesigner.Snippet.Group.Component',
|
||||||
description: 'Koşula göre gösterir veya gizler.',
|
description: 'App.VisualDesigner.Snippet.Visibility.Description',
|
||||||
code: `refs.\${1:card1}.setVisible(refs.${sql}.getMode() === '\${2:edit}')`,
|
code: `refs.\${1:card1}.setVisible(refs.${sql}.getMode() === '\${2:edit}')`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'API çağrısı ve hata yakalama',
|
label: 'App.VisualDesigner.Snippet.ApiCall.Label',
|
||||||
group: 'Entegrasyon',
|
group: 'App.VisualDesigner.Snippet.Group.Integration',
|
||||||
description: 'Çağrı başarısızsa bildirim gösterir.',
|
description: 'App.VisualDesigner.Snippet.ApiCall.Description',
|
||||||
code: `try {\n const sonuc = await api.get('\${1:/api/app/customer/1}')\n refs.${sql}.setField('\${2:Title}', sonuc?.\${3:name})\n} catch (error) {\n notify(api.errorMessage(error), 'danger')\n}`,
|
code: `try {\n const sonuc = await api.get('\${1:/api/app/customer/1}')\n refs.${sql}.setField('\${2:Title}', sonuc?.\${3:name})\n} catch (error) {\n notify(api.errorMessage(error), 'danger')\n}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Satırlar arasında dolaş',
|
label: 'App.VisualDesigner.Snippet.IterateRows.Label',
|
||||||
group: 'Kayıt',
|
group: 'App.VisualDesigner.Snippet.Group.Record',
|
||||||
description: 'Select sonucundaki tüm satırları gezer.',
|
description: 'App.VisualDesigner.Snippet.IterateRows.Description',
|
||||||
code: `refs.${sql}.getRows().forEach((row, index) => {\n \${1:console.log(index, row)}\n})`,
|
code: `refs.${sql}.getRows().forEach((row, index) => {\n \${1:console.log(index, row)}\n})`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Değişiklik varsa kaydet',
|
label: 'App.VisualDesigner.Snippet.SaveIfChanged.Label',
|
||||||
group: 'Kayıt',
|
group: 'App.VisualDesigner.Snippet.Group.Record',
|
||||||
description: 'Boşuna istek atmaz.',
|
description: 'App.VisualDesigner.Snippet.SaveIfChanged.Description',
|
||||||
code: `if (refs.${sql}.hasChanges()) await refs.${sql}.save()`,
|
code: `if (refs.${sql}.hasChanges()) await refs.${sql}.save()`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Event içeriğini incele',
|
label: 'App.VisualDesigner.Snippet.InspectEvent.Label',
|
||||||
group: 'Akış',
|
group: 'App.VisualDesigner.Snippet.Group.Flow',
|
||||||
description: 'Geliştirirken event nesnesinin ne taşıdığını görmek için.',
|
description: 'App.VisualDesigner.Snippet.InspectEvent.Description',
|
||||||
code: `console.log('\${1:event}', JSON.parse(JSON.stringify(event ?? null)))`,
|
code: `console.log('\${1:event}', JSON.parse(JSON.stringify(event ?? null)))`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -143,11 +144,14 @@ export type DesignerScriptDialectContext = {
|
||||||
eventInfo?: SqlDataSourceEvent
|
eventInfo?: SqlDataSourceEvent
|
||||||
/** Event'in sahibi komponentin adı; başlıkta gösterilir. */
|
/** Event'in sahibi komponentin adı; başlıkta gösterilir. */
|
||||||
componentLabel?: string
|
componentLabel?: string
|
||||||
|
/** `useLocalization` hook'undan gelen çeviri fonksiyonu. */
|
||||||
|
translate: TranslateFn
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDesignerScriptDialect = (
|
export const createDesignerScriptDialect = (
|
||||||
context: DesignerScriptDialectContext,
|
context: DesignerScriptDialectContext,
|
||||||
): ScriptDialect => {
|
): ScriptDialect => {
|
||||||
|
const t = context.translate
|
||||||
const sqlContext: DesignerScriptContext = {
|
const sqlContext: DesignerScriptContext = {
|
||||||
sqlRef: context.sqlRef,
|
sqlRef: context.sqlRef,
|
||||||
cancelable: Boolean(context.eventInfo?.cancelable),
|
cancelable: Boolean(context.eventInfo?.cancelable),
|
||||||
|
|
@ -166,12 +170,23 @@ export const createDesignerScriptDialect = (
|
||||||
|
|
||||||
const recipeViews: ScriptRecipeView[] = usableRecipes.map((recipe) => ({
|
const recipeViews: ScriptRecipeView[] = usableRecipes.map((recipe) => ({
|
||||||
id: recipe.id,
|
id: recipe.id,
|
||||||
label: recipe.label,
|
label: t('::' + recipe.label),
|
||||||
group: recipe.group,
|
group: recipe.group,
|
||||||
summary: recipe.summary,
|
summary: t('::' + recipe.summary),
|
||||||
example: recipe.example,
|
example: recipe.example,
|
||||||
conditionIsArgument: recipe.conditionIsArgument,
|
conditionIsArgument: recipe.conditionIsArgument,
|
||||||
params: recipe.params.map(toParam),
|
params: recipe.params.map(toParam).map((param) => ({
|
||||||
|
...param,
|
||||||
|
label: t('::' + param.label),
|
||||||
|
help: param.help ? t('::' + param.help) : undefined,
|
||||||
|
placeholder: param.placeholder?.startsWith('App.')
|
||||||
|
? t('::' + param.placeholder)
|
||||||
|
: param.placeholder,
|
||||||
|
choices: param.choices?.map((choice) => ({
|
||||||
|
...choice,
|
||||||
|
label: choice.label?.startsWith('App.') ? t('::' + choice.label) : choice.label,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const asDesignerRule = (rule: ScriptRule) => rule as unknown as DesignerScriptRule
|
const asDesignerRule = (rule: ScriptRule) => rule as unknown as DesignerScriptRule
|
||||||
|
|
@ -181,25 +196,51 @@ export const createDesignerScriptDialect = (
|
||||||
title: 'Event Script',
|
title: 'Event Script',
|
||||||
subject: `${context.componentLabel ? `${context.componentLabel}.` : ''}${context.eventName}`,
|
subject: `${context.componentLabel ? `${context.componentLabel}.` : ''}${context.eventName}`,
|
||||||
subjectHint: context.eventInfo?.description,
|
subjectHint: context.eventInfo?.description,
|
||||||
codeHint: context.eventInfo?.cancelable
|
codeHint: t(
|
||||||
? 'event / refs / api / notify hazır. false döndürürsen işlem iptal edilir.'
|
context.eventInfo?.cancelable
|
||||||
: 'event / refs / api / notify hazır. Ctrl+Space tamamlama listesini açar.',
|
? '::App.VisualDesigner.CodeHintCancelable'
|
||||||
|
: '::App.VisualDesigner.CodeHint',
|
||||||
|
),
|
||||||
groups: designerRecipeGroups.map((group) => ({
|
groups: designerRecipeGroups.map((group) => ({
|
||||||
key: group.key,
|
key: group.key,
|
||||||
title: group.title,
|
title: t('::' + group.title),
|
||||||
description: group.description,
|
description: t('::' + group.description),
|
||||||
icon: groupIcons[group.key],
|
icon: groupIcons[group.key],
|
||||||
})),
|
})),
|
||||||
recipes: recipeViews,
|
recipes: recipeViews,
|
||||||
operators: DEFAULT_SCRIPT_OPERATORS,
|
operators: DEFAULT_SCRIPT_OPERATORS,
|
||||||
conditionKinds: [
|
conditionKinds: [
|
||||||
{ value: 'record', label: 'Kayıt sütunu', optionSource: RECORD_SOURCE },
|
{
|
||||||
{ value: 'ref', label: 'Komponent değeri', optionSource: REF_SOURCE },
|
value: 'record',
|
||||||
{ value: 'event', label: 'Event verisi', optionSource: EVENT_SOURCE },
|
label: t('::App.VisualDesigner.ConditionKind.RecordColumn'),
|
||||||
{ value: 'expression', label: 'Serbest ifade', placeholder: `refs.${context.sqlRef}.getMode()` },
|
optionSource: RECORD_SOURCE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ref',
|
||||||
|
label: t('::App.VisualDesigner.ConditionKind.ComponentValue'),
|
||||||
|
optionSource: REF_SOURCE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'event',
|
||||||
|
label: t('::App.VisualDesigner.ConditionKind.EventData'),
|
||||||
|
optionSource: EVENT_SOURCE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'expression',
|
||||||
|
label: t('::App.VisualDesigner.ConditionKind.FreeExpression'),
|
||||||
|
placeholder: `refs.${context.sqlRef}.getMode()`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
apiReference: designerRuntimeApiReference,
|
apiReference: designerRuntimeApiReference.map((entry) => ({
|
||||||
snippets: snippets(context.sqlRef),
|
...entry,
|
||||||
|
description: t('::' + entry.description),
|
||||||
|
})),
|
||||||
|
snippets: snippets(context.sqlRef).map((snippet) => ({
|
||||||
|
...snippet,
|
||||||
|
label: t('::' + snippet.label),
|
||||||
|
group: t('::' + snippet.group),
|
||||||
|
description: t('::' + snippet.description),
|
||||||
|
})),
|
||||||
getOptions: (optionSource) =>
|
getOptions: (optionSource) =>
|
||||||
optionSource === RECORD_SOURCE
|
optionSource === RECORD_SOURCE
|
||||||
? recordFields
|
? recordFields
|
||||||
|
|
@ -219,27 +260,28 @@ export const createDesignerScriptDialect = (
|
||||||
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
|
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
|
||||||
},
|
},
|
||||||
describeCondition: (condition) =>
|
describeCondition: (condition) =>
|
||||||
describeDesignerCondition(condition as unknown as DesignerRuleCondition),
|
describeDesignerCondition(condition as unknown as DesignerRuleCondition, t),
|
||||||
describeRule: (rule) => {
|
describeRule: (rule) => {
|
||||||
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
|
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
|
||||||
if (!recipe) return ''
|
if (!recipe) return ''
|
||||||
const action = recipe.describe(asDesignerRule(rule))
|
const action = recipe.describe(asDesignerRule(rule), t)
|
||||||
if (recipe.conditionIsArgument) return action
|
if (recipe.conditionIsArgument) return action
|
||||||
|
const always = t('::App.VisualDesigner.Describe.Always')
|
||||||
const conditions = [rule.condition, ...(rule.conditions ?? [])]
|
const conditions = [rule.condition, ...(rule.conditions ?? [])]
|
||||||
.map((condition) =>
|
.map((condition) =>
|
||||||
describeDesignerCondition(condition as unknown as DesignerRuleCondition),
|
describeDesignerCondition(condition as unknown as DesignerRuleCondition, t),
|
||||||
)
|
)
|
||||||
.filter((text) => text !== 'Her zaman')
|
.filter((text) => text !== always)
|
||||||
const prefix = conditions.length
|
const joiner =
|
||||||
? conditions.join(rule.join === 'or' ? ' veya ' : ' ve ')
|
rule.join === 'or'
|
||||||
: 'Her zaman'
|
? ` ${t('::App.VisualDesigner.Word.Or')} `
|
||||||
|
: ` ${t('::App.VisualDesigner.Word.And')} `
|
||||||
|
const prefix = conditions.length ? conditions.join(joiner) : always
|
||||||
return `${prefix} → ${action}`
|
return `${prefix} → ${action}`
|
||||||
},
|
},
|
||||||
getRuleWarning: (rule) => {
|
getRuleWarning: (rule) => {
|
||||||
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
|
const recipe = designerRecipesById.get(rule.recipe as DesignerRecipeId)
|
||||||
return recipe?.needsSqlRef && !context.sqlRef
|
return recipe?.needsSqlRef && !context.sqlRef ? t('::App.VisualDesigner.SqlRefRequired') : ''
|
||||||
? 'Bu kural bir SqlDataSource ref adı gerektiriyor.'
|
|
||||||
: ''
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -444,25 +444,25 @@ export const SQL_DATA_SOURCE_SLOTS: readonly SqlDataSourceSlot[] = [
|
||||||
property: 'selectEndpoint',
|
property: 'selectEndpoint',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
label: 'Select (GET)',
|
label: 'Select (GET)',
|
||||||
description: 'Kaydı okuyan endpoint. İçerideki komponentler bu sütunlara bağlanır.',
|
description: 'App.VisualDesigner.Event.X.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'insertEndpoint',
|
property: 'insertEndpoint',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
label: 'Insert (POST)',
|
label: 'Insert (POST)',
|
||||||
description: 'Key alanı boşken Save butonu bu endpointi çağırır.',
|
description: 'App.VisualDesigner.Event.X.Description2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'updateEndpoint',
|
property: 'updateEndpoint',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
label: 'Update (PUT)',
|
label: 'Update (PUT)',
|
||||||
description: 'Key alanı doluyken Save butonu bu endpointi çağırır.',
|
description: 'App.VisualDesigner.Event.X.Description3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'deleteEndpoint',
|
property: 'deleteEndpoint',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
label: 'Delete (DELETE)',
|
label: 'Delete (DELETE)',
|
||||||
description: 'Delete butonunu aktifleştirir.',
|
description: 'App.VisualDesigner.Event.X.Description4',
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|
@ -489,101 +489,101 @@ export interface SqlDataSourceEvent {
|
||||||
export const SQL_DATA_SOURCE_EVENTS: readonly SqlDataSourceEvent[] = [
|
export const SQL_DATA_SOURCE_EVENTS: readonly SqlDataSourceEvent[] = [
|
||||||
{
|
{
|
||||||
name: 'onLoad',
|
name: 'onLoad',
|
||||||
label: 'Select tamamlandı',
|
label: 'App.VisualDesigner.Event.OnLoad.Label',
|
||||||
description: 'Select endpointi cevap verdiğinde, satırlar forma yerleştikten sonra çalışır.',
|
description: 'App.VisualDesigner.Event.OnLoad.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'rows', description: 'Select sonucundaki tüm satırlar' },
|
{ key: 'rows', description: 'App.VisualDesigner.Event.Rows.Description' },
|
||||||
{ key: 'count', description: 'Satır sayısı' },
|
{ key: 'count', description: 'App.VisualDesigner.Event.Count.Description' },
|
||||||
{ key: 'record', description: 'Aktif satır' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description' },
|
||||||
{ key: 'index', description: 'Aktif satırın sırası' },
|
{ key: 'index', description: 'App.VisualDesigner.Event.Index.Description' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onRecordChange',
|
name: 'onRecordChange',
|
||||||
label: 'Aktif kayıt değişti',
|
label: 'App.VisualDesigner.Event.OnRecordChange.Label',
|
||||||
description: 'Satırlar arasında gezinildiğinde veya yeni kayıt yüklendiğinde çalışır.',
|
description: 'App.VisualDesigner.Event.OnRecordChange.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'record', description: 'Yeni aktif kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description2' },
|
||||||
{ key: 'index', description: 'Satır sırası' },
|
{ key: 'index', description: 'App.VisualDesigner.Event.Index.Description2' },
|
||||||
{ key: 'rows', description: 'Tüm satırlar' },
|
{ key: 'rows', description: 'App.VisualDesigner.Event.Rows.Description2' },
|
||||||
{ key: 'count', description: 'Satır sayısı' },
|
{ key: 'count', description: 'App.VisualDesigner.Event.Count.Description' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onFieldChange',
|
name: 'onFieldChange',
|
||||||
label: 'Alan değişti',
|
label: 'App.VisualDesigner.Event.OnFieldChange.Label',
|
||||||
description: 'İçerideki bir komponent kayda değer yazdığında çalışır; hesaplama için idealdir.',
|
description: 'App.VisualDesigner.Event.OnFieldChange.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'field', description: 'Değişen sütun adı' },
|
{ key: 'field', description: 'App.VisualDesigner.Event.Field.Description' },
|
||||||
{ key: 'value', description: 'Yeni değer' },
|
{ key: 'value', description: 'App.VisualDesigner.Event.Value.Description' },
|
||||||
{ key: 'previous', description: 'Önceki değer' },
|
{ key: 'previous', description: 'App.VisualDesigner.Event.Previous.Description' },
|
||||||
{ key: 'record', description: 'Değişiklik uygulanmış kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onNewRecord',
|
name: 'onNewRecord',
|
||||||
label: 'Yeni kayıt',
|
label: 'App.VisualDesigner.Event.OnNewRecord.Label',
|
||||||
description: 'Yeni butonuyla boş kayda geçildiğinde çalışır; varsayılan değer yazmak için.',
|
description: 'App.VisualDesigner.Event.OnNewRecord.Description',
|
||||||
payload: [{ key: 'record', description: 'Boş kayıt' }],
|
payload: [{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description4' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onModeChange',
|
name: 'onModeChange',
|
||||||
label: 'Mod değişti',
|
label: 'App.VisualDesigner.Event.OnModeChange.Label',
|
||||||
description: 'Kayıt new/edit modları arasında geçtiğinde çalışır.',
|
description: 'App.VisualDesigner.Event.OnModeChange.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'mode', description: '"new" veya "edit"' },
|
{ key: 'mode', description: '"new" veya "edit"' },
|
||||||
{ key: 'record', description: 'Güncel kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description5' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onBeforeSave',
|
name: 'onBeforeSave',
|
||||||
label: 'Kaydetmeden önce',
|
label: 'App.VisualDesigner.Event.OnBeforeSave.Label',
|
||||||
description: 'Save çağrılmadan önce çalışır. Script false döndürürse kayıt iptal edilir.',
|
description: 'App.VisualDesigner.Event.OnBeforeSave.Description',
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'record', description: 'Kaydedilecek kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description6' },
|
||||||
{ key: 'original', description: 'Yüklendiği andaki hali' },
|
{ key: 'original', description: 'App.VisualDesigner.Event.Original.Description' },
|
||||||
{ key: 'payload', description: 'Endpointe gidecek gövde (Update ise sadece değişenler)' },
|
{ key: 'payload', description: 'App.VisualDesigner.Event.Payload.Description' },
|
||||||
{ key: 'mode', description: '"new" veya "edit"' },
|
{ key: 'mode', description: '"new" veya "edit"' },
|
||||||
{ key: 'isNew', description: 'Insert mi çalışacak?' },
|
{ key: 'isNew', description: 'App.VisualDesigner.Event.IsNew.Description' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onAfterSave',
|
name: 'onAfterSave',
|
||||||
label: 'Kaydettikten sonra',
|
label: 'App.VisualDesigner.Event.OnAfterSave.Label',
|
||||||
description: 'Insert/Update başarıyla tamamlandığında çalışır.',
|
description: 'App.VisualDesigner.Event.OnAfterSave.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'record', description: 'Gönderilen kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description7' },
|
||||||
{ key: 'response', description: 'Endpoint cevabı' },
|
{ key: 'response', description: 'App.VisualDesigner.Event.Response.Description' },
|
||||||
{ key: 'mode', description: 'Kaydetme anındaki mod' },
|
{ key: 'mode', description: 'App.VisualDesigner.Event.Mode.Description' },
|
||||||
{ key: 'isNew', description: 'Insert mi çalıştı?' },
|
{ key: 'isNew', description: 'App.VisualDesigner.Event.IsNew.Description2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onBeforeDelete',
|
name: 'onBeforeDelete',
|
||||||
label: 'Silmeden önce',
|
label: 'App.VisualDesigner.Event.OnBeforeDelete.Label',
|
||||||
description: 'Onay kutusundan önce çalışır. Script false döndürürse silme iptal edilir.',
|
description: 'App.VisualDesigner.Event.OnBeforeDelete.Description',
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'record', description: 'Silinecek kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description8' },
|
||||||
{ key: 'key', description: 'Key sütunundaki değer' },
|
{ key: 'key', description: 'App.VisualDesigner.Event.Key.Description' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onAfterDelete',
|
name: 'onAfterDelete',
|
||||||
label: 'Sildikten sonra',
|
label: 'App.VisualDesigner.Event.OnAfterDelete.Label',
|
||||||
description: 'Delete endpointi başarıyla tamamlandığında çalışır.',
|
description: 'App.VisualDesigner.Event.OnAfterDelete.Description',
|
||||||
payload: [{ key: 'record', description: 'Silinen kayıt' }],
|
payload: [{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description9' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onError',
|
name: 'onError',
|
||||||
label: 'Hata oluştu',
|
label: 'App.VisualDesigner.Event.OnError.Label',
|
||||||
description: 'Select/Insert/Update/Delete sırasında hata alındığında çalışır.',
|
description: 'App.VisualDesigner.Event.OnError.Description',
|
||||||
payload: [
|
payload: [
|
||||||
{ key: 'message', description: 'Kullanıcıya gösterilen hata metni' },
|
{ key: 'message', description: 'App.VisualDesigner.Event.Message.Description' },
|
||||||
{ key: 'action', description: '"reload" | "save" | "delete"' },
|
{ key: 'action', description: '"reload" | "save" | "delete"' },
|
||||||
{ key: 'error', description: 'Ham hata nesnesi' },
|
{ key: 'error', description: 'App.VisualDesigner.Event.Error.Description' },
|
||||||
{ key: 'record', description: 'Hata anındaki kayıt' },
|
{ key: 'record', description: 'App.VisualDesigner.Event.Record.Description10' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
@ -613,7 +613,7 @@ export const getSqlDataSourceKeyField = (node: DesignerNode) =>
|
||||||
*/
|
*/
|
||||||
export const SQL_DATA_SOURCE_KEY_SOURCES = [
|
export const SQL_DATA_SOURCE_KEY_SOURCES = [
|
||||||
{ value: 'query', label: 'URL query string (?id=…)' },
|
{ value: 'query', label: 'URL query string (?id=…)' },
|
||||||
{ value: 'route', label: 'URL son segmenti (/…/5)' },
|
{ value: 'route', label: 'App.VisualDesigner.Event.OnError.Label2' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type SqlDataSourceKeySource = (typeof SQL_DATA_SOURCE_KEY_SOURCES)[number]['value']
|
export type SqlDataSourceKeySource = (typeof SQL_DATA_SOURCE_KEY_SOURCES)[number]['value']
|
||||||
|
|
|
||||||
|
|
@ -67,13 +67,13 @@ export class OrderService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Order creation error:', error)
|
console.error('Order creation error:', error)
|
||||||
|
|
||||||
let errorMessage = 'Sipariş oluşturulurken hata oluştu'
|
let errorMessage = 'Public.payment.createOrderError'
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
errorMessage = error.response.data?.message || errorMessage
|
errorMessage = error.response.data?.message || errorMessage
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
errorMessage = 'Sunucuya bağlanılamadı'
|
errorMessage = 'App.Platform.ServerUnreachable'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ import { toast } from '@/components/ui'
|
||||||
import Notification from '@/components/ui/Notification'
|
import Notification from '@/components/ui/Notification'
|
||||||
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
|
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
|
||||||
|
|
||||||
|
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
|
||||||
|
|
||||||
export class SignalRService {
|
export class SignalRService {
|
||||||
|
/** `useLocalization` hook'undan gelen çeviri fonksiyonu; bildirimlerde kullanılır. */
|
||||||
|
private translate: TranslateFn = (key) => key
|
||||||
private connection!: signalR.HubConnection
|
private connection!: signalR.HubConnection
|
||||||
private isConnected: boolean = false
|
private isConnected: boolean = false
|
||||||
private currentSessionId?: string
|
private currentSessionId?: string
|
||||||
|
|
@ -32,6 +36,10 @@ export class SignalRService {
|
||||||
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
|
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
|
||||||
private onForceCleanup?: () => void
|
private onForceCleanup?: () => void
|
||||||
|
|
||||||
|
setTranslate(translate: TranslateFn) {
|
||||||
|
this.translate = translate
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const { auth } = store.getState()
|
const { auth } = store.getState()
|
||||||
|
|
||||||
|
|
@ -99,9 +107,15 @@ export class SignalRService {
|
||||||
|
|
||||||
this.connection.onreconnected(async () => {
|
this.connection.onreconnected(async () => {
|
||||||
this.isConnected = true
|
this.isConnected = true
|
||||||
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.Reconnected')}
|
||||||
|
type="success"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (this.currentSessionId && store.getState().auth.user) {
|
if (this.currentSessionId && store.getState().auth.user) {
|
||||||
const u = store.getState().auth.user
|
const u = store.getState().auth.user
|
||||||
|
|
@ -112,7 +126,10 @@ export class SignalRService {
|
||||||
this.connection.onclose(async () => {
|
this.connection.onclose(async () => {
|
||||||
if (this.isKicked) {
|
if (this.isKicked) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
|
<Notification
|
||||||
|
title={this.translate('::App.Videoroom.Signal.Reconnecting')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
this.isConnected = false
|
this.isConnected = false
|
||||||
|
|
@ -131,15 +148,27 @@ export class SignalRService {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.connection.on('Error', (message: string) => {
|
this.connection.on('Error', (message: string) => {
|
||||||
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.Error', { message })}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.connection.on('Warning', (message: string) => {
|
this.connection.on('Warning', (message: string) => {
|
||||||
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.Warning', { message })}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.connection.on('Info', (message: string) => {
|
this.connection.on('Info', (message: string) => {
|
||||||
|
|
@ -152,7 +181,7 @@ export class SignalRService {
|
||||||
if (this.isKicked) {
|
if (this.isKicked) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi"
|
title={this.translate('::App.Videoroom.Signal.KickedNoReconnect')}
|
||||||
type="danger"
|
type="danger"
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
@ -163,9 +192,15 @@ export class SignalRService {
|
||||||
|
|
||||||
this.connection.on('ForceDisconnect', async (message: string) => {
|
this.connection.on('ForceDisconnect', async (message: string) => {
|
||||||
this.isKicked = true
|
this.isKicked = true
|
||||||
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.Kicked', { message })}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (this.onForceCleanup) {
|
if (this.onForceCleanup) {
|
||||||
this.onForceCleanup()
|
this.onForceCleanup()
|
||||||
|
|
@ -194,18 +229,24 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
const startPromise = this.connection.start()
|
const startPromise = this.connection.start()
|
||||||
const timeout = new Promise((_, reject) =>
|
const timeout = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
|
setTimeout(
|
||||||
|
() => reject(new Error(this.translate('::App.Videoroom.Signal.Timeout'))),
|
||||||
|
10000,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.race([startPromise, timeout])
|
await Promise.race([startPromise, timeout])
|
||||||
this.isConnected = true
|
this.isConnected = true
|
||||||
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.Signal.Connected')} type="success" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin."
|
title={this.translate('::App.Videoroom.Signal.ServerUnreachable')}
|
||||||
type="danger"
|
type="danger"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -224,7 +265,7 @@ export class SignalRService {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
|
title={this.translate('::App.Videoroom.Signal.NoConnection')}
|
||||||
type="warning"
|
type="warning"
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
|
|
@ -238,9 +279,12 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
|
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.Signal.JoinFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,9 +300,15 @@ export class SignalRService {
|
||||||
await this.connection.invoke('LeaveClass', sessionId)
|
await this.connection.invoke('LeaveClass', sessionId)
|
||||||
this.currentSessionId = undefined
|
this.currentSessionId = undefined
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.LeaveFailed')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,9 +347,15 @@ export class SignalRService {
|
||||||
'public',
|
'public',
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.PublicMessageFailed')}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,9 +400,15 @@ export class SignalRService {
|
||||||
'private',
|
'private',
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.PrivateMessageFailed')}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,9 +446,12 @@ export class SignalRService {
|
||||||
isTeacher,
|
isTeacher,
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.AnnouncementFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,9 +471,12 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
|
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.MuteFailed')} type="warning" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,9 +491,15 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
|
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.RaiseHandFailed')}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,9 +514,12 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('KickParticipant', sessionId, participantId)
|
await this.connection.invoke('KickParticipant', sessionId, participantId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.KickFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,9 +534,15 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
|
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.HandRaiseApproveFailed')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,9 +557,15 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
|
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.LowerHandFailed')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,9 +643,15 @@ export class SignalRService {
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('LeaveClass', this.currentSessionId)
|
await this.connection.invoke('LeaveClass', this.currentSessionId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Signal.DisconnectError')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
import { toast } from '@/components/ui'
|
import { toast } from '@/components/ui'
|
||||||
import Notification from '@/components/ui/Notification'
|
import Notification from '@/components/ui/Notification'
|
||||||
|
|
||||||
|
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
|
||||||
|
|
||||||
export class WebRTCService {
|
export class WebRTCService {
|
||||||
|
/** `useLocalization` hook'undan gelen çeviri fonksiyonu; bildirimlerde kullanılır. */
|
||||||
|
private translate: TranslateFn = (key) => key
|
||||||
private peerConnections: Map<string, RTCPeerConnection> = new Map()
|
private peerConnections: Map<string, RTCPeerConnection> = new Map()
|
||||||
private retryCounts: Map<string, number> = new Map()
|
private retryCounts: Map<string, number> = new Map()
|
||||||
private maxRetries = 3
|
private maxRetries = 3
|
||||||
private signalRService: any
|
private signalRService: any
|
||||||
private sessionId: string = ''
|
private sessionId: string = ''
|
||||||
|
|
||||||
|
setTranslate(translate: TranslateFn) {
|
||||||
|
this.translate = translate
|
||||||
|
}
|
||||||
|
|
||||||
private localStream: MediaStream | null = null
|
private localStream: MediaStream | null = null
|
||||||
private onRemoteStream?: (userId: string, stream: MediaStream) => void
|
private onRemoteStream?: (userId: string, stream: MediaStream) => void
|
||||||
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
|
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
|
||||||
|
|
@ -50,7 +58,7 @@ export class WebRTCService {
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title="❌ Kamera/Mikrofon erişilemedi. Tarayıcı ayarlarınızı veya izinleri kontrol edin."
|
title={this.translate('::App.Videoroom.Rtc.MediaAccessFailed')}
|
||||||
type="danger"
|
type="danger"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -93,7 +101,10 @@ export class WebRTCService {
|
||||||
if (retries < this.maxRetries) {
|
if (retries < this.maxRetries) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title={`⚠️ Bağlantı başarısız, yeniden deneniyor (${retries + 1}/${this.maxRetries})`}
|
title={this.translate('::App.Videoroom.Rtc.RetryingConnection', {
|
||||||
|
current: retries + 1,
|
||||||
|
total: this.maxRetries,
|
||||||
|
})}
|
||||||
type="warning"
|
type="warning"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -103,7 +114,9 @@ export class WebRTCService {
|
||||||
} else {
|
} else {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title={`❌ Bağlantı kurulamadı (${this.maxRetries} deneme başarısız).`}
|
title={this.translate('::App.Videoroom.Rtc.ConnectionFailed', {
|
||||||
|
total: this.maxRetries,
|
||||||
|
})}
|
||||||
type="danger"
|
type="danger"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -120,7 +133,7 @@ export class WebRTCService {
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
|
title={this.translate('::App.Videoroom.Rtc.IceCandidateFailed', { userId })}
|
||||||
type="warning"
|
type="warning"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -151,9 +164,12 @@ export class WebRTCService {
|
||||||
await pc.setLocalDescription(offer)
|
await pc.setLocalDescription(offer)
|
||||||
return offer
|
return offer
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Offer oluşturulamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.Rtc.OfferFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
throw new Error('Offer creation failed')
|
throw new Error('Offer creation failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,9 +187,12 @@ export class WebRTCService {
|
||||||
await pc.setLocalDescription(answer)
|
await pc.setLocalDescription(answer)
|
||||||
return answer
|
return answer
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Answer oluşturulamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.Rtc.AnswerFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
throw new Error('Answer creation failed')
|
throw new Error('Answer creation failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +219,7 @@ export class WebRTCService {
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
|
title={this.translate('::App.Videoroom.Rtc.IceCandidateFailed', { userId })}
|
||||||
type="warning"
|
type="warning"
|
||||||
/>,
|
/>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -240,9 +259,12 @@ export class WebRTCService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Kamera açılamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification title={this.translate('::App.Videoroom.Rtc.CameraFailed')} type="danger" />,
|
||||||
})
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,9 +291,15 @@ export class WebRTCService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Rtc.MicrophoneFailed')}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,14 +316,26 @@ export class WebRTCService {
|
||||||
if (this.signalRService) {
|
if (this.signalRService) {
|
||||||
await this.signalRService.sendOffer(this.sessionId, userId, offer)
|
await this.signalRService.sendOffer(this.sessionId, userId, offer)
|
||||||
} else {
|
} else {
|
||||||
toast.push(<Notification title="⚠️ Tekrar bağlanma başarısız" type="warning" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Rtc.ReconnectFailed')}
|
||||||
|
type="warning"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification
|
||||||
})
|
title={this.translate('::App.Videoroom.Rtc.IceRestartFailed')}
|
||||||
|
type="danger"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const useOrders = () => {
|
||||||
setError(null)
|
setError(null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Products fetch error:', err)
|
console.error('Products fetch error:', err)
|
||||||
setError('Ürünler yüklenirken hata oluştu')
|
setError('App.Orders.LoadProductsError')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export function usePWA(): boolean {
|
export function usePWA(): boolean {
|
||||||
const [isPWA, setIsPWA] = useState(false);
|
const [isPWA, setIsPWA] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkPWA = () => {
|
const checkPWA = () => {
|
||||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone;
|
const isStandalone =
|
||||||
setIsPWA(isStandalone);
|
window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
};
|
(window.navigator as any).standalone
|
||||||
|
setIsPWA(isStandalone)
|
||||||
|
}
|
||||||
|
|
||||||
checkPWA(); // İlk kontrolü yap
|
checkPWA() // İlk kontrolü yap
|
||||||
|
|
||||||
// display-mode değiştiğinde güncellenmesi için event listener ekle
|
// display-mode değiştiğinde güncellenmesi için event listener ekle
|
||||||
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
|
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Event listener'ı temizle
|
// Event listener'ı temizle
|
||||||
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
|
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA)
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return isPWA;
|
return isPWA
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from "react-icons/fi";
|
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from 'react-icons/fi'
|
||||||
|
|
||||||
export const kindOptions = [
|
export const kindOptions = [
|
||||||
{ value: "Start", label: "Başlat" },
|
{ value: 'Start', label: 'ListForms.Workflow.Kind.Start' },
|
||||||
{ value: "Compare", label: "Karşılaştırma" },
|
{ value: 'Compare', label: 'ListForms.Workflow.Kind.Compare' },
|
||||||
{ value: "Approval", label: "Onaylanacak kişi" },
|
{ value: 'Approval', label: 'ListForms.Workflow.Kind.Approval' },
|
||||||
{ value: "Inform", label: "Bilgilendirme" },
|
{ value: 'Inform', label: 'ListForms.Workflow.Kind.Inform' },
|
||||||
{ value: "End", label: "Akışı bitir" },
|
{ value: 'End', label: 'ListForms.Workflow.Kind.End' },
|
||||||
];
|
]
|
||||||
|
|
||||||
export const operatorOptions = [">", ">=", "<", "<=", "=", "!="].map(
|
export const operatorOptions = ['>', '>=', '<', '<=', '=', '!='].map((value) => ({
|
||||||
(value) => ({
|
value,
|
||||||
value,
|
label: value,
|
||||||
label: value,
|
}))
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const kindIcon: Record<string, any> = {
|
export const kindIcon: Record<string, any> = {
|
||||||
Start: FiPlay as any,
|
Start: FiPlay as any,
|
||||||
|
|
@ -21,13 +19,13 @@ export const kindIcon: Record<string, any> = {
|
||||||
Approval: FiCheck as any,
|
Approval: FiCheck as any,
|
||||||
Inform: FiBell as any,
|
Inform: FiBell as any,
|
||||||
End: FiSlash as any,
|
End: FiSlash as any,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const nodeSize = {
|
export const nodeSize = {
|
||||||
width: 176,
|
width: 176,
|
||||||
height: 128,
|
height: 128,
|
||||||
};
|
}
|
||||||
|
|
||||||
export function getNodeHeight(item: { kind: string }) {
|
export function getNodeHeight(item: { kind: string }) {
|
||||||
return item?.kind === "Compare" ? 158 : nodeSize.height;
|
return item?.kind === 'Compare' ? 158 : nodeSize.height
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,27 +153,27 @@ export function collectLinks(criteria: WorkflowCriteriaDto[]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addLink(links, criteria, source, source.nextOnStart, 'Sonraki', 'next', {
|
addLink(links, criteria, source, source.nextOnStart, OUTCOME_KEYS.nextOnStart, 'next', {
|
||||||
index: 0,
|
index: 0,
|
||||||
count: 1,
|
count: 1,
|
||||||
field: 'nextOnStart',
|
field: 'nextOnStart',
|
||||||
})
|
})
|
||||||
addLink(links, criteria, source, source.nextOnTrue, 'Doğru', 'true', {
|
addLink(links, criteria, source, source.nextOnTrue, OUTCOME_KEYS.nextOnTrue, 'true', {
|
||||||
index: 0,
|
index: 0,
|
||||||
count: 2,
|
count: 2,
|
||||||
field: 'nextOnTrue',
|
field: 'nextOnTrue',
|
||||||
})
|
})
|
||||||
addLink(links, criteria, source, source.nextOnFalse, 'Yanlış', 'false', {
|
addLink(links, criteria, source, source.nextOnFalse, OUTCOME_KEYS.nextOnFalse, 'false', {
|
||||||
index: 1,
|
index: 1,
|
||||||
count: 2,
|
count: 2,
|
||||||
field: 'nextOnFalse',
|
field: 'nextOnFalse',
|
||||||
})
|
})
|
||||||
addLink(links, criteria, source, source.nextOnApprove, 'Onay', 'approve', {
|
addLink(links, criteria, source, source.nextOnApprove, OUTCOME_KEYS.nextOnApprove, 'approve', {
|
||||||
index: 0,
|
index: 0,
|
||||||
count: 2,
|
count: 2,
|
||||||
field: 'nextOnApprove',
|
field: 'nextOnApprove',
|
||||||
})
|
})
|
||||||
addLink(links, criteria, source, source.nextOnReject, 'Red', 'reject', {
|
addLink(links, criteria, source, source.nextOnReject, OUTCOME_KEYS.nextOnReject, 'reject', {
|
||||||
index: 1,
|
index: 1,
|
||||||
count: 2,
|
count: 2,
|
||||||
field: 'nextOnReject',
|
field: 'nextOnReject',
|
||||||
|
|
@ -242,43 +242,53 @@ function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
|
||||||
return dy >= 0 ? 'bottom' : 'top'
|
return dy >= 0 ? 'bottom' : 'top'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Çıkış etiketleri lokalizasyon anahtarı olarak taşınır; ekranda `translate` ile çözülür. */
|
||||||
|
export const OUTCOME_KEYS = {
|
||||||
|
nextOnStart: 'ListForms.Workflow.Outcome.Next',
|
||||||
|
nextOnTrue: 'ListForms.Workflow.Outcome.True',
|
||||||
|
nextOnFalse: 'ListForms.Workflow.Outcome.False',
|
||||||
|
nextOnApprove: 'ListForms.Workflow.Outcome.Approve',
|
||||||
|
nextOnReject: 'ListForms.Workflow.Outcome.Reject',
|
||||||
|
compare: 'ListForms.Workflow.Outcome.CompareState',
|
||||||
|
} as const
|
||||||
|
|
||||||
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
|
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
|
||||||
if (item.kind === 'Compare') {
|
if (item.kind === 'Compare') {
|
||||||
const outcomes = item.compareOutcomes?.length
|
const outcomes = item.compareOutcomes?.length
|
||||||
? item.compareOutcomes
|
? item.compareOutcomes
|
||||||
: [
|
: [
|
||||||
{ label: 'Doğru', targetId: item.nextOnTrue },
|
{ label: OUTCOME_KEYS.nextOnTrue, targetId: item.nextOnTrue },
|
||||||
{ label: 'Yanlış', targetId: item.nextOnFalse },
|
{ label: OUTCOME_KEYS.nextOnFalse, targetId: item.nextOnFalse },
|
||||||
]
|
]
|
||||||
|
|
||||||
return outcomes.slice(0, 4).map((outcome, index) => ({
|
return outcomes.slice(0, 4).map((outcome, index) => ({
|
||||||
field: `compareOutcomes:${index}`,
|
field: `compareOutcomes:${index}`,
|
||||||
label: outcome.label || `Durum ${index + 1}`,
|
label: outcome.label || `#${index + 1}`,
|
||||||
targetId: outcome.targetId,
|
targetId: outcome.targetId,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === 'Approval') {
|
if (item.kind === 'Approval') {
|
||||||
return [
|
return [
|
||||||
{ field: 'nextOnApprove', label: 'Onay', targetId: item.nextOnApprove },
|
{ field: 'nextOnApprove', label: OUTCOME_KEYS.nextOnApprove, targetId: item.nextOnApprove },
|
||||||
{ field: 'nextOnReject', label: 'Red', targetId: item.nextOnReject },
|
{ field: 'nextOnReject', label: OUTCOME_KEYS.nextOnReject, targetId: item.nextOnReject },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === 'End') return []
|
if (item.kind === 'End') return []
|
||||||
|
|
||||||
return [{ field: 'nextOnStart', label: 'Sonraki', targetId: item.nextOnStart }]
|
return [{ field: 'nextOnStart', label: OUTCOME_KEYS.nextOnStart, targetId: item.nextOnStart }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function outcomeLabel(field?: string) {
|
export function outcomeLabel(field?: string) {
|
||||||
if (field?.startsWith('compareOutcomes:')) return 'Karşılaştırma durumu'
|
if (field?.startsWith('compareOutcomes:')) return OUTCOME_KEYS.compare
|
||||||
|
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
nextOnStart: 'Sonraki',
|
nextOnStart: OUTCOME_KEYS.nextOnStart,
|
||||||
nextOnTrue: 'Doğru',
|
nextOnTrue: OUTCOME_KEYS.nextOnTrue,
|
||||||
nextOnFalse: 'Yanlış',
|
nextOnFalse: OUTCOME_KEYS.nextOnFalse,
|
||||||
nextOnApprove: 'Onay',
|
nextOnApprove: OUTCOME_KEYS.nextOnApprove,
|
||||||
nextOnReject: 'Red',
|
nextOnReject: OUTCOME_KEYS.nextOnReject,
|
||||||
}
|
}
|
||||||
|
|
||||||
return field ? labels[field] : undefined
|
return field ? labels[field] : undefined
|
||||||
|
|
@ -406,7 +416,9 @@ function formatCriteriaId(number: number) {
|
||||||
|
|
||||||
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
|
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
|
||||||
const normalized = (title || '').trim()
|
const normalized = (title || '').trim()
|
||||||
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
|
return (
|
||||||
|
normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeRegExp(value: string) {
|
function escapeRegExp(value: string) {
|
||||||
|
|
@ -432,8 +444,7 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
||||||
const sharedPerson =
|
const sharedPerson = item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
|
||||||
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
|
|
||||||
const compareOutcomes = (item.compareOutcomes || [])
|
const compareOutcomes = (item.compareOutcomes || [])
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
.filter((outcome) => outcome.label?.trim())
|
.filter((outcome) => outcome.label?.trim())
|
||||||
|
|
@ -465,12 +476,12 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
|
||||||
export function defaultTitle(kind: string) {
|
export function defaultTitle(kind: string) {
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
Start: 'İş Akışı Başlat',
|
Start: 'ListForms.Workflow.Kind.Start',
|
||||||
Compare: 'Karşılaştırma',
|
Compare: 'ListForms.Workflow.Kind.Compare',
|
||||||
Approval: 'Onay',
|
Approval: 'ListForms.Workflow.Kind.Approval',
|
||||||
Inform: 'Bilgilendirme',
|
Inform: 'ListForms.Workflow.Kind.Inform',
|
||||||
End: 'İş Akışı Bitir',
|
End: 'ListForms.Workflow.Kind.End',
|
||||||
}[kind] ?? 'İş Akışı Adımı'
|
}[kind] ?? 'ListForms.Workflow.Kind.Step'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,7 +581,7 @@ export function compareOutcomeRuleText(
|
||||||
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
|
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
|
||||||
)
|
)
|
||||||
.join(' ve ')
|
.join(' ve ')
|
||||||
: 'Kural yok'
|
: 'ListForms.Workflow.NoRule'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCompactValue(value: number | string | null | undefined) {
|
export function formatCompactValue(value: number | string | null | undefined) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ import { useStoreState } from '@/store'
|
||||||
* Kullanıcı bilinçli olarak kurulum sihirbazına yönlendirilmez — çalışan bir sistemde
|
* Kullanıcı bilinçli olarak kurulum sihirbazına yönlendirilmez — çalışan bir sistemde
|
||||||
* geçici bir arıza kimseyi kurulum ekranına düşürmemeli. Bunun yerine durum açıkça
|
* geçici bir arıza kimseyi kurulum ekranına düşürmemeli. Bunun yerine durum açıkça
|
||||||
* belirtilir, sunucu arka planda izlenir ve toparlandığı anda sayfa kendini yeniler.
|
* belirtilir, sunucu arka planda izlenir ve toparlandığı anda sayfa kendini yeniler.
|
||||||
|
*
|
||||||
|
* Metinler bilinçli olarak `translate` ile lokalize EDİLMEZ: bu ekran yalnızca
|
||||||
|
* konfigürasyon (ve onunla gelen lokalizasyon sözlüğü) yüklenemediğinde render edilir,
|
||||||
|
* dolayısıyla `translate` anahtarın kendisini döndürürdü. Açılış öncesi tüm ekranlar
|
||||||
|
* bu nedenle sabit İngilizce tutulur.
|
||||||
*/
|
*/
|
||||||
const ServiceUnavailable = () => {
|
const ServiceUnavailable = () => {
|
||||||
const isDark = useStoreState((state) => state.theme.mode) === 'dark'
|
const isDark = useStoreState((state) => state.theme.mode) === 'dark'
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary'
|
||||||
import type { FallbackProps } from 'react-error-boundary'
|
import type { FallbackProps } from 'react-error-boundary'
|
||||||
import { FaArrowLeft } from 'react-icons/fa'
|
import { FaArrowLeft } from 'react-icons/fa'
|
||||||
import { Navigate, useLocation } from 'react-router-dom'
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import DialogProvider from './shared/DialogContext'
|
import DialogProvider from './shared/DialogContext'
|
||||||
import DialogShowComponent from './shared/DialogContext/DialogShowComponent'
|
import DialogShowComponent from './shared/DialogContext/DialogShowComponent'
|
||||||
import UiDialog from './shared/UiDialog'
|
import UiDialog from './shared/UiDialog'
|
||||||
|
|
@ -20,13 +21,16 @@ interface ViewsProps {
|
||||||
|
|
||||||
// react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor;
|
// react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor;
|
||||||
// throw edilen her şey Error olmayabilir, o yüzden daraltıyoruz.
|
// throw edilen her şey Error olmayabilir, o yüzden daraltıyoruz.
|
||||||
function fallbackRender({ error, resetErrorBoundary }: FallbackProps) {
|
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const { name, message } =
|
const { name, message } =
|
||||||
error instanceof Error ? error : { name: 'Hata!', message: String(error) }
|
error instanceof Error
|
||||||
|
? error
|
||||||
|
: { name: translate('::App.Platform.Error'), message: String(error) }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert showIcon className="mb-4" type="danger">
|
<Alert showIcon className="mb-4" type="danger">
|
||||||
<h5>{name ?? 'Hata!'}</h5>
|
<h5>{name ?? translate('::App.Platform.Error')}</h5>
|
||||||
<div>{message}</div>
|
<div>{message}</div>
|
||||||
<Button
|
<Button
|
||||||
icon={<FaArrowLeft />}
|
icon={<FaArrowLeft />}
|
||||||
|
|
@ -49,7 +53,7 @@ const Views = (props: ViewsProps) => {
|
||||||
}, [location, setWarning])
|
}, [location, setWarning])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallbackRender={fallbackRender}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<Suspense fallback={<Loading loading={true} />}>
|
<Suspense fallback={<Loading loading={true} />}>
|
||||||
{!!warning?.length && (
|
{!!warning?.length && (
|
||||||
<Alert showIcon className="mb-4 text-sm text-left" type="warning">
|
<Alert showIcon className="mb-4 text-sm text-left" type="warning">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { NotificationChannelColors } from '@/constants/notification-channel.enum
|
||||||
import { NotificationDto } from '@/proxy/notification/models'
|
import { NotificationDto } from '@/proxy/notification/models'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
type EventProps = {
|
type EventProps = {
|
||||||
data: NotificationDto
|
data: NotificationDto
|
||||||
|
|
@ -19,6 +20,8 @@ const HighlightedText = ({ children, className }: CommonProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Event = ({ data, compact }: EventProps) => {
|
const Event = ({ data, compact }: EventProps) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{compact ? (
|
{compact ? (
|
||||||
|
|
@ -28,7 +31,11 @@ const Event = ({ data, compact }: EventProps) => {
|
||||||
<span className="text-xs">{dayjs(data.creationTime).format('hh:mm A')}</span>
|
<span className="text-xs">{dayjs(data.creationTime).format('hh:mm A')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<span className="mx-1">bildirim gönderdi {data.notificationType} </span>
|
<span className="mx-1">
|
||||||
|
{translate('::Abp.Identity.ActivityLogs.SentNotificationOfType', {
|
||||||
|
type: data.notificationType,
|
||||||
|
})}{' '}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Tag
|
<Tag
|
||||||
prefix
|
prefix
|
||||||
|
|
@ -42,7 +49,8 @@ const Event = ({ data, compact }: EventProps) => {
|
||||||
) : (
|
) : (
|
||||||
<div className="my-1 flex items-center">
|
<div className="my-1 flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<HighlightedText>{data.notificationType}</HighlightedText> bildirimi gönderdi.
|
<HighlightedText>{data.notificationType}</HighlightedText>{' '}
|
||||||
|
{translate('::Abp.Identity.ActivityLogs.SentNotification')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tag
|
<Tag
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ const Log = ({
|
||||||
</div>
|
</div>
|
||||||
<Timeline className="dark:bg-gray-800">
|
<Timeline className="dark:bg-gray-800">
|
||||||
{isEmpty(notifications[group]) ? (
|
{isEmpty(notifications[group]) ? (
|
||||||
<Timeline.Item className="dark:text-gray-400">Bildirim yok</Timeline.Item>
|
<Timeline.Item className="dark:text-gray-400">
|
||||||
|
{translate('::Abp.Identity.ActivityLogs.NoNotification')}
|
||||||
|
</Timeline.Item>
|
||||||
) : (
|
) : (
|
||||||
notifications[group].map((notification) => (
|
notifications[group].map((notification) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
|
|
@ -56,11 +58,13 @@ const Log = ({
|
||||||
))}
|
))}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{loadable ? (
|
{loadable ? (
|
||||||
<Button size="sm" loading={isLoading} variant='solid' onClick={onLoadMore}>
|
<Button size="sm" loading={isLoading} variant="solid" onClick={onLoadMore}>
|
||||||
{translate('::Abp.Identity.ActivityLogs.LoadMore')}
|
{translate('::Abp.Identity.ActivityLogs.LoadMore')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<span className="dark:text-gray-400">{translate('::Abp.Identity.ActivityLogs.ReceivedAllNotifications')}</span>
|
<span className="dark:text-gray-400">
|
||||||
|
{translate('::Abp.Identity.ActivityLogs.ReceivedAllNotifications')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,15 @@ const LogFilter = ({
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className={classNames('rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4', className)}>
|
<div
|
||||||
<h5 className="mb-4 text-base font-semibold text-gray-900 dark:text-white">{translate('::Abp.Identity.ActivityLogs.Filters')}</h5>
|
className={classNames(
|
||||||
|
'rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h5 className="mb-4 text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{translate('::Abp.Identity.ActivityLogs.Filters')}
|
||||||
|
</h5>
|
||||||
<Checkbox.Group
|
<Checkbox.Group
|
||||||
vertical
|
vertical
|
||||||
value={filter}
|
value={filter}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,15 @@ const getFileTypeLabel = (item: FileItemType, translate: (key: string) => string
|
||||||
|
|
||||||
// Defined at module level: nesting it inside FileItem would remount the <img>
|
// Defined at module level: nesting it inside FileItem would remount the <img>
|
||||||
// (and restart the download) on every parent render.
|
// (and restart the download) on every parent render.
|
||||||
const ImagePreview = ({ src, alt, fallback }: { src: string; alt: string; fallback: ReactNode }) => {
|
const ImagePreview = ({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
fallback: ReactNode
|
||||||
|
}) => {
|
||||||
const [imageError, setImageError] = useState(false)
|
const [imageError, setImageError] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -250,16 +258,16 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||||
navigator.clipboard.writeText(fileUrl).then(
|
navigator.clipboard.writeText(fileUrl).then(
|
||||||
() => {
|
() => {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" title="Başarılı">
|
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||||
Dosya URL'si panoya kopyalandı
|
{translate('::FileManager.UrlCopied')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" title="Hata">
|
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||||
URL kopyalanamadı
|
{translate('::FileManager.UrlCopyFailed')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -438,7 +446,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||||
</span>
|
</span>
|
||||||
) : item.type === 'folder' && typeof item.childCount === 'number' ? (
|
) : item.type === 'folder' && typeof item.childCount === 'number' ? (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{item.childCount} öğe
|
{translate('::FileManager.ItemCount', { count: item.childCount })}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">-</span>
|
<span className="text-sm text-gray-500 dark:text-gray-400">-</span>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,11 @@ export const CreateFolderModal = forwardRef<HTMLDivElement, CreateFolderModalPro
|
||||||
<Dialog isOpen={isOpen} onClose={handleClose}>
|
<Dialog isOpen={isOpen} onClose={handleClose}>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<form className="py-6" onSubmit={handleSubmit}>
|
<form className="py-6" onSubmit={handleSubmit}>
|
||||||
<FormItem label={translate('::FileManager.FolderName')} invalid={!!error} errorMessage={error}>
|
<FormItem
|
||||||
|
label={translate('::FileManager.FolderName')}
|
||||||
|
invalid={!!error}
|
||||||
|
errorMessage={error}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
value={folderName}
|
value={folderName}
|
||||||
|
|
@ -58,11 +62,11 @@ export const CreateFolderModal = forwardRef<HTMLDivElement, CreateFolderModalPro
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4 border-t">
|
<div className="flex justify-end space-x-2 pt-4 border-t">
|
||||||
<Button size='sm' variant="default" disabled={loading} onClick={handleClose}>
|
<Button size="sm" variant="default" disabled={loading} onClick={handleClose}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size="sm"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!folderName.trim()}
|
disabled={!folderName.trim()}
|
||||||
|
|
@ -156,11 +160,11 @@ export const RenameItemModal = forwardRef<HTMLDivElement, RenameItemModalProps>(
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4 border-t">
|
<div className="flex justify-end space-x-2 pt-4 border-t">
|
||||||
<Button size='sm' variant="default" disabled={loading} onClick={handleClose}>
|
<Button size="sm" variant="default" disabled={loading} onClick={handleClose}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size="sm"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!newName.trim()}
|
disabled={!newName.trim()}
|
||||||
|
|
@ -239,15 +243,10 @@ export const DeleteConfirmModal = forwardRef<HTMLDivElement, DeleteConfirmModalP
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4 border-t">
|
<div className="flex justify-end space-x-2 pt-4 border-t">
|
||||||
<Button size='sm' variant="default" disabled={loading} onClick={onClose}>
|
<Button size="sm" variant="default" disabled={loading} onClick={onClose}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="solid" loading={loading} onClick={handleDelete}>
|
||||||
size='sm'
|
|
||||||
variant="solid"
|
|
||||||
loading={loading}
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
{translate('::Delete')}
|
{translate('::Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -651,6 +651,7 @@ function OrgChartTree({
|
||||||
setPositions: Dispatch<SetStateAction<PositionMap>>
|
setPositions: Dispatch<SetStateAction<PositionMap>>
|
||||||
edges: EdgeLink[]
|
edges: EdgeLink[]
|
||||||
}) {
|
}) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const nodeRefs = useRef<NodeRefMap>({})
|
const nodeRefs = useRef<NodeRefMap>({})
|
||||||
const cardRefs = useRef<CardRefMap>({})
|
const cardRefs = useRef<CardRefMap>({})
|
||||||
|
|
@ -765,7 +766,7 @@ function OrgChartTree({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-24 text-slate-400">
|
<div className="flex flex-col items-center justify-center py-24 text-slate-400">
|
||||||
<FaSitemap className="w-12 h-12 mb-4 opacity-30" />
|
<FaSitemap className="w-12 h-12 mb-4 opacity-30" />
|
||||||
<p className="text-sm">Veri bulunamadı</p>
|
<p className="text-sm">{translate('::App.NoDataFound')}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -874,7 +875,7 @@ const OrgChart = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title="Organizasyon Şeması" />
|
<PageTitle title={translate('::App.Definitions.OrgChart')} />
|
||||||
|
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
|
|
@ -882,7 +883,7 @@ const OrgChart = () => {
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{MenuIcon}
|
{MenuIcon}
|
||||||
<h4 className="text-sm font-medium truncate text-gray-900 dark:text-white">
|
<h4 className="text-sm font-medium truncate text-gray-900 dark:text-white">
|
||||||
{translate('::App.Definitions.OrgChart') || 'Organizasyon Şeması'}
|
{translate('::App.Definitions.OrgChart')}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -900,9 +901,7 @@ const OrgChart = () => {
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setMode('department')}
|
onClick={() => setMode('department')}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">{translate('::App.Hr.Department')}</span>
|
||||||
{translate('::App.Hr.Department') || 'Departman'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -916,9 +915,7 @@ const OrgChart = () => {
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setMode('jobPosition')}
|
onClick={() => setMode('jobPosition')}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">{translate('::App.Hr.JobPosition')}</span>
|
||||||
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -931,7 +928,7 @@ const OrgChart = () => {
|
||||||
onChange={(e) => setShowUsers(e.target.checked)}
|
onChange={(e) => setShowUsers(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}
|
{translate('::App.Definitions.OrgChart.ShowUsers')}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -972,10 +969,12 @@ const OrgChart = () => {
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaFileImage className="h-3.5 w-3.5 flex-shrink-0" />}
|
icon={<FaFileImage className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
className="!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-lg bg-slate-100 !p-1.5 text-xs font-medium text-slate-600 transition-colors hover:!bg-slate-200 disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700 sm:!p-2 sm:text-sm"
|
className="!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-lg bg-slate-100 !p-1.5 text-xs font-medium text-slate-600 transition-colors hover:!bg-slate-200 disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700 sm:!p-2 sm:text-sm"
|
||||||
title="JPG olarak indir"
|
title={translate('::App.Definitions.OrgChart.ExportJpg')}
|
||||||
onClick={handleExportJpg}
|
onClick={handleExportJpg}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{exporting ? translate('::App.Platform.Processing') : 'Export'}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ function ChartTabSeries(props: FormEditProps & { listFormCode: string }) {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,8 @@ function FormCustomization({
|
||||||
validate={(values) => {
|
validate={(values) => {
|
||||||
if (values.userId && values.roleId) {
|
if (values.userId && values.roleId) {
|
||||||
return {
|
return {
|
||||||
userId: 'Hem rol hem user seçilemez',
|
userId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
roleId: 'Hem rol hem user seçilemez',
|
roleId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +177,7 @@ function FormCustomization({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -312,7 +312,7 @@ function FormCustomization({
|
||||||
>
|
>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
|
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
|
|
||||||
<div className="text-right mt-6">
|
<div className="text-right mt-6">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ const FormEdit = () => {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -240,8 +240,7 @@ const FormEdit = () => {
|
||||||
{listFormValues.isTenant && (
|
{listFormValues.isTenant && (
|
||||||
<Badge
|
<Badge
|
||||||
className="font-semibold dark:bg-red-500 dark:text-white"
|
className="font-semibold dark:bg-red-500 dark:text-white"
|
||||||
content="Bu bir MULTI TENANT form'dur, veri kaybı olmaması için, sorgularda TENANTID
|
content={translate('::ListForms.ListFormEdit.MultiTenantWarning')}
|
||||||
parametresini kullanmayı unutmayınız."
|
|
||||||
innerClass="p-1 bg-red-50 text-red-500"
|
innerClass="p-1 bg-red-50 text-red-500"
|
||||||
></Badge>
|
></Badge>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -103,14 +103,26 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
if (selectCommandColumns.length > 0) return
|
if (selectCommandColumns.length > 0) return
|
||||||
|
|
||||||
const table = dbObjects.tables.find((t) => t.tableName === cmd)
|
const table = dbObjects.tables.find((t) => t.tableName === cmd)
|
||||||
if (table) { loadColumns(dsCode, table.schemaName, table.tableName); return }
|
if (table) {
|
||||||
|
loadColumns(dsCode, table.schemaName, table.tableName)
|
||||||
|
return
|
||||||
|
}
|
||||||
const view = dbObjects.views.find((v) => v.objectName === cmd)
|
const view = dbObjects.views.find((v) => v.objectName === cmd)
|
||||||
if (view) { loadColumns(dsCode, view.schemaName, view.objectName); return }
|
if (view) {
|
||||||
|
loadColumns(dsCode, view.schemaName, view.objectName)
|
||||||
|
return
|
||||||
|
}
|
||||||
const fn = dbObjects.functions.find((f) => f.objectName === cmd)
|
const fn = dbObjects.functions.find((f) => f.objectName === cmd)
|
||||||
if (fn) { loadColumns(dsCode, fn.schemaName, fn.objectName); return }
|
if (fn) {
|
||||||
|
loadColumns(dsCode, fn.schemaName, fn.objectName)
|
||||||
|
return
|
||||||
|
}
|
||||||
const sp = dbObjects.storedProcedures.find((p) => p.objectName === cmd)
|
const sp = dbObjects.storedProcedures.find((p) => p.objectName === cmd)
|
||||||
if (sp) { loadColumns(dsCode, sp.schemaName, sp.objectName); return }
|
if (sp) {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
loadColumns(dsCode, sp.schemaName, sp.objectName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dbObjects])
|
}, [dbObjects])
|
||||||
|
|
||||||
if (!initialValues) {
|
if (!initialValues) {
|
||||||
|
|
@ -295,7 +307,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
form.setFieldValue('keyFieldName', '')
|
form.setFieldValue('keyFieldName', '')
|
||||||
form.setFieldValue('keyFieldDbSourceType', undefined)
|
form.setFieldValue('keyFieldDbSourceType', undefined)
|
||||||
if (!option.__isNew__ && option.__schema != null && option.__rawName) {
|
if (!option.__isNew__ && option.__schema != null && option.__rawName) {
|
||||||
loadColumns(values.dataSourceCode ?? '', option.__schema, option.__rawName)
|
loadColumns(
|
||||||
|
values.dataSourceCode ?? '',
|
||||||
|
option.__schema,
|
||||||
|
option.__rawName,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setSelectCommandColumns([])
|
setSelectCommandColumns([])
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +340,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
placeholder={translate('::App.Listform.ListformField.TableName')}
|
placeholder={translate('::App.Listform.ListformField.TableName')}
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::App.Listform.ListformField.KeyFieldName')}
|
label={translate('::App.Listform.ListformField.KeyFieldName')}
|
||||||
invalid={errors.keyFieldName && touched.keyFieldName}
|
invalid={errors.keyFieldName && touched.keyFieldName}
|
||||||
|
|
@ -338,8 +354,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
</span>
|
</span>
|
||||||
) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
|
) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand') +
|
{translate('::ListForms.ListFormEdit.ColumnsLoadAfterSelect', {
|
||||||
' seçince sütunlar yüklenir'}
|
command: translate(
|
||||||
|
'::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand',
|
||||||
|
),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
@ -355,9 +374,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
placeholder={
|
placeholder={
|
||||||
isLoadingColumns
|
isLoadingColumns
|
||||||
? translate('::Loading')
|
? translate('::Loading')
|
||||||
: translate(
|
: translate('::App.Listform.ListformField.KeyFieldName')
|
||||||
'::App.Listform.ListformField.KeyFieldName',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
options={selectCommandColumns.map((c) => ({
|
options={selectCommandColumns.map((c) => ({
|
||||||
label: `${c.columnName} (${c.dataType})`,
|
label: `${c.columnName} (${c.dataType})`,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ function FormTabGantt(props: FormEditProps) {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ function FormTabScheduler(props: FormEditProps) {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ function FormTabSubForm() {
|
||||||
<Td>{row.tabTitle}</Td>
|
<Td>{row.tabTitle}</Td>
|
||||||
<Td>{row.tabType}</Td>
|
<Td>{row.tabType}</Td>
|
||||||
<Td>{row.code}</Td>
|
<Td>{row.code}</Td>
|
||||||
<Td>{row.isRefresh ? 'Evet' : 'Hayır'}</Td>
|
<Td>{translate(row.isRefresh ? '::App.Platform.Yes' : '::App.Platform.No')}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<ul className="list-disc">
|
<ul className="list-disc">
|
||||||
{row.relation?.map((i) => (
|
{row.relation?.map((i) => (
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ function FormTabTree(props: FormEditProps) {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
@ -184,7 +184,14 @@ function FormTabTree(props: FormEditProps) {
|
||||||
component={Checkbox}
|
component={Checkbox}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit" className="my-2">
|
<Button
|
||||||
|
block
|
||||||
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
loading={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
className="my-2"
|
||||||
|
>
|
||||||
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ function FormFieldTabConditionalFormatting({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -206,7 +206,7 @@ function FormFieldTabConditionalFormatting({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const schema = object().shape({
|
||||||
.nullable()
|
.nullable()
|
||||||
.test(
|
.test(
|
||||||
'is-json-object',
|
'is-json-object',
|
||||||
'editorOptions geçerli bir JSON nesnesi olmalıdır.',
|
'ListForms.ListFormFieldEdit.EditorOptionsMustBeJsonObject',
|
||||||
(value) => !value?.trim() || isValidJsonText(value),
|
(value) => !value?.trim() || isValidJsonText(value),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
@ -126,21 +126,15 @@ function FormFieldTabDetails({
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsAlignment')}>
|
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsAlignment')}>
|
||||||
<SelectField
|
<SelectField name="alignment" options={listFormAlignmentOptions} />
|
||||||
name="alignment"
|
|
||||||
options={listFormAlignmentOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsFormat')}>
|
<FormItem label={translate('::ListForms.ListFormFieldEdit.DetailsFormat')}>
|
||||||
<SelectField
|
<SelectField name="format" options={tooltipFormatListOptions} />
|
||||||
name="format"
|
|
||||||
options={tooltipFormatListOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::ListForms.ListFormFieldEdit.DetailsEditorOptions')}
|
label={translate('::ListForms.ListFormFieldEdit.DetailsEditorOptions')}
|
||||||
invalid={errors.editorOptions && touched.editorOptions}
|
invalid={errors.editorOptions && touched.editorOptions}
|
||||||
errorMessage={errors.editorOptions}
|
errorMessage={errors.editorOptions && translate('::' + errors.editorOptions)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 items-start">
|
<div className="flex gap-2 items-start">
|
||||||
<Field
|
<Field
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ function TablePickerModal({
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||||
{step === 'table'
|
{step === 'table'
|
||||||
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
|
? translate('::ListForms.Wizard.Step3.SelectTable')
|
||||||
: (pickerTable?.tableName ?? '')}
|
: (pickerTable?.tableName ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,7 +99,7 @@ function TablePickerModal({
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
value={tableSearch}
|
value={tableSearch}
|
||||||
placeholder={translate('::Search') || 'Ara...'}
|
placeholder={translate('::Search')}
|
||||||
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
onChange={(e) => setTableSearch(e.target.value)}
|
onChange={(e) => setTableSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -107,7 +107,7 @@ function TablePickerModal({
|
||||||
<div className="overflow-y-auto flex-1 p-2">
|
<div className="overflow-y-auto flex-1 p-2">
|
||||||
{!dbObjects ? (
|
{!dbObjects ? (
|
||||||
<div className="text-xs text-gray-400 text-center py-6">
|
<div className="text-xs text-gray-400 text-center py-6">
|
||||||
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') || 'Tablo bulunamadı'}
|
{translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
dbObjects.tables
|
dbObjects.tables
|
||||||
|
|
@ -160,14 +160,14 @@ function TablePickerModal({
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||||
Key Sütunu
|
{translate('::ListForms.ListFormFieldEdit.KeyColumn')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={keyCol}
|
value={keyCol}
|
||||||
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
onChange={(e) => setKeyCol(e.target.value)}
|
onChange={(e) => setKeyCol(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">-- Seçiniz --</option>
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{pickerColumns.map((c) => (
|
{pickerColumns.map((c) => (
|
||||||
<option key={c.columnName} value={c.columnName}>
|
<option key={c.columnName} value={c.columnName}>
|
||||||
{c.columnName}
|
{c.columnName}
|
||||||
|
|
@ -177,14 +177,14 @@ function TablePickerModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||||
Name Sütunu
|
{translate('::ListForms.ListFormFieldEdit.NameColumn')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={nameCol}
|
value={nameCol}
|
||||||
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
onChange={(e) => setNameCol(e.target.value)}
|
onChange={(e) => setNameCol(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">-- Seçiniz --</option>
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{pickerColumns.map((c) => (
|
{pickerColumns.map((c) => (
|
||||||
<option key={c.columnName} value={c.columnName}>
|
<option key={c.columnName} value={c.columnName}>
|
||||||
{c.columnName}
|
{c.columnName}
|
||||||
|
|
@ -305,7 +305,7 @@ function FormFieldTabLookup({
|
||||||
className="ml-2 !h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40"
|
className="ml-2 !h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40"
|
||||||
onClick={openTablePicker}
|
onClick={openTablePicker}
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
{translate('::ListForms.Wizard.Step3.GenerateFromTable')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
|
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
|
||||||
|
|
@ -430,7 +430,14 @@ function FormFieldTabLookup({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Button block size="sm" className="mt-1" variant="solid" loading={isSubmitting} type="submit">
|
<Button
|
||||||
|
block
|
||||||
|
size="sm"
|
||||||
|
className="mt-1"
|
||||||
|
variant="solid"
|
||||||
|
loading={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ function FormFieldTabValidationRules({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -216,7 +216,7 @@ function FormFieldTabValidationRules({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -235,10 +235,7 @@ function FormFieldTabValidationRules({
|
||||||
invalid={errors.type && touched.type}
|
invalid={errors.type && touched.type}
|
||||||
errorMessage={errors.type}
|
errorMessage={errors.type}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="type" options={columnValidationTypeListOptions} />
|
||||||
name="type"
|
|
||||||
options={columnValidationTypeListOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -328,7 +325,14 @@ function FormFieldTabValidationRules({
|
||||||
<Field name="trim" placeholder="Trim" component={Checkbox} />
|
<Field name="trim" placeholder="Trim" component={Checkbox} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<Button block size="sm" className="mt-1" variant="solid" loading={isLoading} type="submit">
|
<Button
|
||||||
|
block
|
||||||
|
size="sm"
|
||||||
|
className="mt-1"
|
||||||
|
variant="solid"
|
||||||
|
loading={isLoading}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{isLoading ? translate('::Saving') : translate('::Save')}
|
{isLoading ? translate('::Saving') : translate('::Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ function FormFields({
|
||||||
if (newCols.length === 0) {
|
if (newCols.length === 0) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="info" duration={2000}>
|
<Notification type="info" duration={2000}>
|
||||||
Tüm sütunlar zaten eklenmiş.
|
{translate('::ListForms.ListFormField.AllColumnsAlreadyAdded')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -182,7 +182,7 @@ function FormFields({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{newCols.length} sütun eklendi.
|
{translate('::ListForms.ListFormField.ColumnsAdded', { count: newCols.length })}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -250,7 +250,7 @@ function FormFields({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
@ -298,7 +298,7 @@ function FormFields({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -324,8 +324,8 @@ function FormFields({
|
||||||
validate={(values) => {
|
validate={(values) => {
|
||||||
if (values.userId && values.roleId) {
|
if (values.userId && values.roleId) {
|
||||||
return {
|
return {
|
||||||
userId: 'Hem rol hem user seçilemez',
|
userId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
roleId: 'Hem rol hem user seçilemez',
|
roleId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,7 +336,7 @@ function FormFields({
|
||||||
getFields(cultureName, roleId, userId).then(() => {
|
getFields(cultureName, roleId, userId).then(() => {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={100}>
|
<Notification type="success" duration={100}>
|
||||||
{'Alanlar geldi'}
|
{translate('::ListForms.ListFormField.FieldsLoaded')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -419,7 +419,7 @@ function FormFields({
|
||||||
variant="plain"
|
variant="plain"
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Tüm Sütunları Ekle"
|
title={translate('::ListForms.ListFormField.AddAllColumns')}
|
||||||
icon={<FaTable />}
|
icon={<FaTable />}
|
||||||
loading={isAddingAllColumns}
|
loading={isAddingAllColumns}
|
||||||
onClick={addAllColumns}
|
onClick={addAllColumns}
|
||||||
|
|
@ -517,8 +517,8 @@ function FormFields({
|
||||||
validate={(values) => {
|
validate={(values) => {
|
||||||
if (values.userId && values.roleId) {
|
if (values.userId && values.roleId) {
|
||||||
return {
|
return {
|
||||||
userId: 'Hem rol hem user seçilemez',
|
userId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
roleId: 'Hem rol hem user seçilemez',
|
roleId: translate('::ListForms.RoleOrUserOnly'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,7 +547,7 @@ function FormFields({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -582,10 +582,7 @@ function FormFields({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label={translate('::ListForms.ListFormEdit.CustomValueType')}>
|
<FormItem label={translate('::ListForms.ListFormEdit.CustomValueType')}>
|
||||||
<SelectField
|
<SelectField name="sourceDbType" options={dbSourceTypeOptions} />
|
||||||
name="sourceDbType"
|
|
||||||
options={dbSourceTypeOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -594,10 +591,7 @@ function FormFields({
|
||||||
errorMessage={errors.cultureName}
|
errorMessage={errors.cultureName}
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="cultureName" options={langOptions} />
|
||||||
name="cultureName"
|
|
||||||
options={langOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -605,10 +599,7 @@ function FormFields({
|
||||||
invalid={errors.roleId && touched.roleId}
|
invalid={errors.roleId && touched.roleId}
|
||||||
errorMessage={errors.roleId}
|
errorMessage={errors.roleId}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="roleId" options={roleList} />
|
||||||
name="roleId"
|
|
||||||
options={roleList}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -616,10 +607,7 @@ function FormFields({
|
||||||
invalid={errors.userId && touched.userId}
|
invalid={errors.userId && touched.userId}
|
||||||
errorMessage={errors.userId}
|
errorMessage={errors.userId}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="userId" options={userList} />
|
||||||
name="userId"
|
|
||||||
options={userList}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
@ -637,8 +625,8 @@ function FormFields({
|
||||||
onClose={() => setDeleteRowId(null)}
|
onClose={() => setDeleteRowId(null)}
|
||||||
onRequestClose={() => setDeleteRowId(null)}
|
onRequestClose={() => setDeleteRowId(null)}
|
||||||
>
|
>
|
||||||
<h5 className="mb-4">Sil</h5>
|
<h5 className="mb-4">{translate('::App.Platform.Delete')}</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<div className="text-right mt-6">
|
<div className="text-right mt-6">
|
||||||
<Button
|
<Button
|
||||||
className="ltr:mr-2 rtl:ml-2"
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
|
@ -714,14 +702,19 @@ function FormFields({
|
||||||
try {
|
try {
|
||||||
await postListFormFieldCopy({ ...values })
|
await postListFormFieldCopy({ ...values })
|
||||||
|
|
||||||
toast.push(<Notification type="success">{'Field Kopyalandı.'}</Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::ListForms.ListFormField.FieldCopied')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleCopyFieldFormClose()
|
handleCopyFieldFormClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -739,10 +732,7 @@ function FormFields({
|
||||||
invalid={errors.sourceFieldName && touched.sourceFieldName}
|
invalid={errors.sourceFieldName && touched.sourceFieldName}
|
||||||
errorMessage={errors.sourceFieldName}
|
errorMessage={errors.sourceFieldName}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="sourceFieldName" options={fieldList} />
|
||||||
name="sourceFieldName"
|
|
||||||
options={fieldList}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -764,10 +754,7 @@ function FormFields({
|
||||||
invalid={errors.sourceCultureName && touched.sourceCultureName}
|
invalid={errors.sourceCultureName && touched.sourceCultureName}
|
||||||
errorMessage={errors.sourceCultureName}
|
errorMessage={errors.sourceCultureName}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="sourceCultureName" options={langOptions} />
|
||||||
name="sourceCultureName"
|
|
||||||
options={langOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -775,10 +762,7 @@ function FormFields({
|
||||||
invalid={errors.targetCultureName && touched.targetCultureName}
|
invalid={errors.targetCultureName && touched.targetCultureName}
|
||||||
errorMessage={errors.targetCultureName}
|
errorMessage={errors.targetCultureName}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="targetCultureName" options={langOptions} />
|
||||||
name="targetCultureName"
|
|
||||||
options={langOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -786,10 +770,7 @@ function FormFields({
|
||||||
invalid={errors.roleId && touched.roleId}
|
invalid={errors.roleId && touched.roleId}
|
||||||
errorMessage={errors.roleId}
|
errorMessage={errors.roleId}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="roleId" options={roleList} />
|
||||||
name="roleId"
|
|
||||||
options={roleList}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -797,10 +778,7 @@ function FormFields({
|
||||||
invalid={errors.userId && touched.userId}
|
invalid={errors.userId && touched.userId}
|
||||||
errorMessage={errors.userId}
|
errorMessage={errors.userId}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField name="userId" options={userList} />
|
||||||
name="userId"
|
|
||||||
options={userList}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<FormItem label={translate('::App.Reports.Search')} labelClass="justify-center">
|
<FormItem label={translate('::App.Reports.Search')} labelClass="justify-center">
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,8 @@ function EditorOptionsBuilderDialog({
|
||||||
const visiblePresets = useMemo(
|
const visiblePresets = useMemo(
|
||||||
() =>
|
() =>
|
||||||
editorOptionsPresets.filter(
|
editorOptionsPresets.filter(
|
||||||
(preset) => showAll || !preset.editors || !editorType || preset.editors.includes(editorType),
|
(preset) =>
|
||||||
|
showAll || !preset.editors || !editorType || preset.editors.includes(editorType),
|
||||||
),
|
),
|
||||||
[editorType, showAll],
|
[editorType, showAll],
|
||||||
)
|
)
|
||||||
|
|
@ -326,9 +327,9 @@ function EditorOptionsBuilderDialog({
|
||||||
<h5 className="mr-1">Editor Options</h5>
|
<h5 className="mr-1">Editor Options</h5>
|
||||||
<span
|
<span
|
||||||
className="rounded-full bg-indigo-100 px-2.5 py-[2px] text-[11px] font-medium text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200"
|
className="rounded-full bg-indigo-100 px-2.5 py-[2px] text-[11px] font-medium text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200"
|
||||||
title="Alanlar bu editör tipine göre süzülüyor. Editör tipi Seçenekler sekmesinden değiştirilir."
|
title={translate('::ListForms.EditorOptions.EditorTypeFilterHint')}
|
||||||
>
|
>
|
||||||
{editorType || 'editör tipi seçilmemiş'}
|
{editorType || translate('::ListForms.EditorOptions.NoEditorType')}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-2.5 py-[2px] text-[11px] font-medium ${
|
className={`rounded-full px-2.5 py-[2px] text-[11px] font-medium ${
|
||||||
|
|
@ -336,9 +337,9 @@ function EditorOptionsBuilderDialog({
|
||||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200'
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200'
|
||||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
|
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
title="JSON'a yazılacak ayar sayısı"
|
title={translate('::ListForms.EditorOptions.SetCountHint')}
|
||||||
>
|
>
|
||||||
{setCount} ayar
|
{translate('::ListForms.EditorOptions.SettingCount', { count: setCount })}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
|
@ -346,11 +347,11 @@ function EditorOptionsBuilderDialog({
|
||||||
<button
|
<button
|
||||||
disabled={!!rawError}
|
disabled={!!rawError}
|
||||||
type="button"
|
type="button"
|
||||||
title={rawError ? 'Önce JSON hatasını düzelt.' : undefined}
|
title={rawError ? translate('::ListForms.EditorOptions.FixJsonFirst') : undefined}
|
||||||
className={tabButtonClass(tab === 'builder')}
|
className={tabButtonClass(tab === 'builder')}
|
||||||
onClick={() => goToTab('builder')}
|
onClick={() => goToTab('builder')}
|
||||||
>
|
>
|
||||||
<FaSlidersH /> Ayarlar
|
<FaSlidersH /> {translate('::ListForms.EditorOptions.SettingsTab')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -365,19 +366,18 @@ function EditorOptionsBuilderDialog({
|
||||||
|
|
||||||
{rawError && (
|
{rawError && (
|
||||||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
||||||
JSON geçersiz: {rawError} — düzeltilene kadar kaydedilemez. Mevcut değer korunuyor.
|
{translate('::ListForms.EditorOptions.InvalidJsonBanner', {
|
||||||
|
error: translate('::' + rawError),
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'json' ? (
|
{tab === 'json' ? (
|
||||||
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||||
<div className="flex items-center justify-between gap-2 text-xs text-gray-500">
|
<div className="flex items-center justify-between gap-2 text-xs text-gray-500">
|
||||||
<span>
|
<span>{translate('::ListForms.EditorOptions.JsonTabHint')}</span>
|
||||||
Buradaki JSON doğrudan editorOptions alanına yazılır. Geçerli olduğu anda Ayarlar
|
|
||||||
sekmesi de güncellenir.
|
|
||||||
</span>
|
|
||||||
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyPreview}>
|
<Button size="sm" type="button" icon={<FaCopy />} onClick={copyPreview}>
|
||||||
{copied ? 'Kopyalandı' : 'Kopyala'}
|
{translate(copied ? '::App.Platform.Copied' : '::App.SqlQueryManager.Copy')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -397,47 +397,47 @@ function EditorOptionsBuilderDialog({
|
||||||
<input
|
<input
|
||||||
className={`${controlClass} !w-64`}
|
className={`${controlClass} !w-64`}
|
||||||
value={search}
|
value={search}
|
||||||
placeholder="Ayar ara (mask, format, search…)"
|
placeholder={translate('::ListForms.EditorOptions.SearchSetting')}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
|
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
|
||||||
title="Kapalıyken yalnızca seçili editöre uyan ve halihazırda dolu olan ayarlar listelenir."
|
title={translate('::ListForms.EditorOptions.ShowAllHint')}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showAll}
|
checked={showAll}
|
||||||
onChange={(event) => setShowAll(event.target.checked)}
|
onChange={(event) => setShowAll(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
Tüm ayarlar
|
{translate('::ListForms.EditorOptions.AllSettings')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
|
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-2.5 py-1.5 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-300"
|
||||||
title="Yalnızca JSON'da değeri olan ayarları göster."
|
title={translate('::ListForms.EditorOptions.OnlySetHint')}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={onlySet}
|
checked={onlySet}
|
||||||
onChange={(event) => setOnlySet(event.target.checked)}
|
onChange={(event) => setOnlySet(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
Sadece dolu
|
{translate('::ListForms.EditorOptions.OnlySet')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<Button size="sm" type="button" onClick={() => setAllSections(true)}>
|
<Button size="sm" type="button" onClick={() => setAllSections(true)}>
|
||||||
Tümünü aç
|
{translate('::ListForms.EditorOptions.ExpandAll')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" type="button" onClick={() => setAllSections(false)}>
|
<Button size="sm" type="button" onClick={() => setAllSections(false)}>
|
||||||
Tümünü kapat
|
{translate('::ListForms.EditorOptions.CollapseAll')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
title="Tüm ayarları temizler. Kaydedilirse editorOptions boşalır."
|
title={translate('::ListForms.EditorOptions.ClearAllHint')}
|
||||||
icon={<FaEraser />}
|
icon={<FaEraser />}
|
||||||
onClick={() => setOptions({})}
|
onClick={() => setOptions({})}
|
||||||
>
|
>
|
||||||
Temizle
|
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -450,8 +450,8 @@ function EditorOptionsBuilderDialog({
|
||||||
<div className={cardClass}>
|
<div className={cardClass}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
icon={FaMagic}
|
icon={FaMagic}
|
||||||
title="Hazır Ayarlar"
|
title={translate('::ListForms.EditorOptions.Presets')}
|
||||||
description="Seçilen kalıp mevcut JSON ile birleştirilir; diğer ayarlarını silmez."
|
description={translate('::ListForms.EditorOptions.PresetsHint')}
|
||||||
badge={`${visiblePresets.length}`}
|
badge={`${visiblePresets.length}`}
|
||||||
open={isSectionOpen(PRESETS_KEY)}
|
open={isSectionOpen(PRESETS_KEY)}
|
||||||
onToggle={() => toggleSection(PRESETS_KEY)}
|
onToggle={() => toggleSection(PRESETS_KEY)}
|
||||||
|
|
@ -463,10 +463,10 @@ function EditorOptionsBuilderDialog({
|
||||||
key={preset.key}
|
key={preset.key}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
title={preset.description}
|
title={translate('::' + preset.description)}
|
||||||
onClick={() => setOptions((current) => ({ ...current, ...preset.value }))}
|
onClick={() => setOptions((current) => ({ ...current, ...preset.value }))}
|
||||||
>
|
>
|
||||||
{preset.label}
|
{translate('::' + preset.label)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -480,15 +480,17 @@ function EditorOptionsBuilderDialog({
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
accent={group.setCount > 0}
|
accent={group.setCount > 0}
|
||||||
icon={groupIcons[group.key]}
|
icon={groupIcons[group.key]}
|
||||||
title={group.title}
|
title={translate('::' + group.title)}
|
||||||
description={group.description}
|
description={translate('::' + group.description)}
|
||||||
badge={`${group.setCount}/${group.specs.length}`}
|
badge={`${group.setCount}/${group.specs.length}`}
|
||||||
open={open}
|
open={open}
|
||||||
onToggle={() => toggleSection(group.key)}
|
onToggle={() => toggleSection(group.key)}
|
||||||
/>
|
/>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
||||||
<p className="mb-3 text-[11px] text-gray-400">{group.description}</p>
|
<p className="mb-3 text-[11px] text-gray-400">
|
||||||
|
{translate('::' + group.description)}
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-3 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-x-4 gap-y-3 md:grid-cols-3">
|
||||||
{group.specs.map((spec) => (
|
{group.specs.map((spec) => (
|
||||||
<OptionField
|
<OptionField
|
||||||
|
|
@ -507,8 +509,7 @@ function EditorOptionsBuilderDialog({
|
||||||
|
|
||||||
{!groupedSpecs.length && (
|
{!groupedSpecs.length && (
|
||||||
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
|
<div className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-xs text-gray-400 dark:border-gray-700">
|
||||||
Filtreye uyan ayar yok. "Tüm ayarlar" kutusunu işaretleyerek tüm
|
{translate('::ListForms.EditorOptions.NoMatchingSetting')}
|
||||||
sözlüğü görebilirsin.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -517,8 +518,8 @@ function EditorOptionsBuilderDialog({
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
accent={unmanagedPaths.length > 0}
|
accent={unmanagedPaths.length > 0}
|
||||||
icon={FaPuzzlePiece}
|
icon={FaPuzzlePiece}
|
||||||
title="Sözlükte Olmayan Ayarlar"
|
title={translate('::ListForms.EditorOptions.UnmanagedSettings')}
|
||||||
description="JSON'da bulunan ama listede karşılığı olmayan her şey burada görünür."
|
description={translate('::ListForms.EditorOptions.UnmanagedSettingsHint')}
|
||||||
badge={`${unmanagedPaths.length}`}
|
badge={`${unmanagedPaths.length}`}
|
||||||
open={isSectionOpen(UNMANAGED_KEY)}
|
open={isSectionOpen(UNMANAGED_KEY)}
|
||||||
onToggle={() => toggleSection(UNMANAGED_KEY)}
|
onToggle={() => toggleSection(UNMANAGED_KEY)}
|
||||||
|
|
@ -526,8 +527,7 @@ function EditorOptionsBuilderDialog({
|
||||||
{isSectionOpen(UNMANAGED_KEY) && (
|
{isSectionOpen(UNMANAGED_KEY) && (
|
||||||
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
<div className="border-t border-gray-100 px-3 py-3 dark:border-gray-800">
|
||||||
<p className="mb-3 text-[11px] text-gray-400">
|
<p className="mb-3 text-[11px] text-gray-400">
|
||||||
Hiçbir ayar gizli kalmaz. DevExtreme dokümanındaki herhangi bir yolu buradan
|
{translate('::ListForms.EditorOptions.UnmanagedHelp')}
|
||||||
ekleyebilirsin (örn: toolbar.multiline).
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{unmanagedPaths.map((path) => {
|
{unmanagedPaths.map((path) => {
|
||||||
|
|
@ -573,7 +573,7 @@ function EditorOptionsBuilderDialog({
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Bu ayarı sil"
|
title={translate('::ListForms.EditorOptions.DeleteSetting')}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setOptions((previous) => unsetByPath(previous, path))
|
setOptions((previous) => unsetByPath(previous, path))
|
||||||
|
|
@ -604,7 +604,11 @@ function EditorOptionsBuilderDialog({
|
||||||
<input
|
<input
|
||||||
className={`${controlClass} col-span-5`}
|
className={`${controlClass} col-span-5`}
|
||||||
value={newValue}
|
value={newValue}
|
||||||
placeholder={newType === 'boolean' ? 'true / false' : 'değer'}
|
placeholder={
|
||||||
|
newType === 'boolean'
|
||||||
|
? 'true / false'
|
||||||
|
: translate('::ListForms.EditorOptions.Value')
|
||||||
|
}
|
||||||
onChange={(event) => setNewValue(event.target.value)}
|
onChange={(event) => setNewValue(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
|
|
@ -619,7 +623,7 @@ function EditorOptionsBuilderDialog({
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="Ayarı ekle"
|
title={translate('::ListForms.EditorOptions.AddSetting')}
|
||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
onClick={addCustomOption}
|
onClick={addCustomOption}
|
||||||
/>
|
/>
|
||||||
|
|
@ -635,18 +639,22 @@ function EditorOptionsBuilderDialog({
|
||||||
<div className="mb-3 flex items-center justify-between gap-2">
|
<div className="mb-3 flex items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2 text-sm font-semibold">
|
<span className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<FaCode className="text-gray-400" />
|
<FaCode className="text-gray-400" />
|
||||||
JSON Önizleme
|
{translate('::ListForms.EditorOptions.JsonPreview')}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] font-normal text-gray-500 dark:bg-gray-800 dark:text-gray-300">
|
<span className="rounded-full bg-gray-100 px-2 py-[2px] text-[11px] font-normal text-gray-500 dark:bg-gray-800 dark:text-gray-300">
|
||||||
{preview ? `${stringifyOptions(options).length} karakter` : 'boş'}
|
{preview
|
||||||
|
? translate('::ListForms.EditorOptions.CharCount', {
|
||||||
|
count: stringifyOptions(options).length,
|
||||||
|
})
|
||||||
|
: translate('::ListForms.EditorOptions.Empty')}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
title="JSON'u panoya kopyala"
|
title={translate('::ListForms.EditorOptions.CopyJson')}
|
||||||
icon={copied ? <FaCheck /> : <FaCopy />}
|
icon={copied ? <FaCheck /> : <FaCopy />}
|
||||||
onClick={copyPreview}
|
onClick={copyPreview}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ function EditorScriptBuilderDialog({
|
||||||
// sabitlenmezse editör yazarken sıfırlanırdı.
|
// sabitlenmezse editör yazarken sıfırlanırdı.
|
||||||
const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|')
|
const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|')
|
||||||
const dialect = useMemo(
|
const dialect = useMemo(
|
||||||
() => createFormScriptDialect({ currentField, fields: fieldKey.split('|') }),
|
() => createFormScriptDialect({ currentField, fields: fieldKey.split('|'), translate }),
|
||||||
[currentField, fieldKey],
|
[currentField, fieldKey, translate],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,11 @@ function JsonRowOpDialogAnnotation({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -155,7 +159,7 @@ function JsonRowOpDialogAnnotation({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -449,14 +453,19 @@ function JsonRowOpDialogAnnotation({
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -470,7 +479,7 @@ function JsonRowOpDialogAnnotation({
|
||||||
<Form className="flex flex-col h-full">
|
<Form className="flex flex-col h-full">
|
||||||
<Dialog.Body className="flex flex-col gap-2">
|
<Dialog.Body className="flex flex-col gap-2">
|
||||||
<h5>Delete</h5>
|
<h5>Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||||
<Button size="sm" variant="plain" onClick={handleClose}>
|
<Button size="sm" variant="plain" onClick={handleClose}>
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,11 @@ function JsonRowOpDialogAxis({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -170,7 +174,7 @@ function JsonRowOpDialogAxis({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -477,21 +481,26 @@ function JsonRowOpDialogAxis({
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -505,7 +514,12 @@ function JsonRowOpDialogAxis({
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,11 @@ function JsonRowOpDialogCommand({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -147,7 +151,7 @@ function JsonRowOpDialogCommand({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -278,7 +282,7 @@ function JsonRowOpDialogCommand({
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<Tabs defaultValue="urlOptions" variant="pill">
|
<Tabs defaultValue="urlOptions" variant="pill">
|
||||||
<TabList>
|
<TabList>
|
||||||
<TabNav value="urlOptions">Url Options</TabNav>
|
<TabNav value="urlOptions">Url Options</TabNav>
|
||||||
|
|
@ -366,7 +370,12 @@ function JsonRowOpDialogCommand({
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
@ -382,21 +391,26 @@ function JsonRowOpDialogCommand({
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -411,8 +425,13 @@ function JsonRowOpDialogCommand({
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
{ translate('::Cancel') }
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
{isSubmitting ? 'Deleting' : 'Delete'}
|
{isSubmitting ? 'Deleting' : 'Delete'}
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,11 @@ function JsonRowOpDialogDatabase({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -129,7 +133,7 @@ function JsonRowOpDialogDatabase({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -239,8 +243,13 @@ function JsonRowOpDialogDatabase({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
{ translate('::Cancel') }
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
||||||
|
|
@ -255,21 +264,26 @@ function JsonRowOpDialogDatabase({
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -284,8 +298,13 @@ function JsonRowOpDialogDatabase({
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
{ translate('::Cancel') }
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
{isSubmitting ? 'Deleting' : 'Delete'}
|
{isSubmitting ? 'Deleting' : 'Delete'}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,11 @@ function JsonRowOpDialogEditForm({
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi</Notification>)
|
toast.push(
|
||||||
|
<Notification type="success">
|
||||||
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
)
|
||||||
await handleClose()
|
await handleClose()
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
|
|
@ -65,24 +69,46 @@ function JsonRowOpDialogEditForm({
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<Dialog.Body><p>{translate('::DeleteConfirmation')}</p></Dialog.Body>
|
<Dialog.Body>
|
||||||
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
|
</Dialog.Body>
|
||||||
<Dialog.Footer className="flex justify-end gap-2">
|
<Dialog.Footer className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button>
|
<Button type="button" variant="plain" onClick={handleClose}>
|
||||||
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Delete')}</Button>
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="solid" loading={isSubmitting}>
|
||||||
|
{translate('::Delete')}
|
||||||
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
) : (
|
) : (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data.editFormValues ?? { order: 1, itemType: 'group', caption: '', colCount: 1, colSpan: 1 }}
|
initialValues={
|
||||||
|
data.editFormValues ?? {
|
||||||
|
order: 1,
|
||||||
|
itemType: 'group',
|
||||||
|
caption: '',
|
||||||
|
colCount: 1,
|
||||||
|
colSpan: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
try {
|
try {
|
||||||
const input: ListFormJsonRowDto = { index: data.index, fieldName: data.tabName, itemEditingForm: values }
|
const input: ListFormJsonRowDto = {
|
||||||
|
index: data.index,
|
||||||
|
fieldName: data.tabName,
|
||||||
|
itemEditingForm: values,
|
||||||
|
}
|
||||||
if (data.index === -1) await postListFormJsonRow(data.id, input)
|
if (data.index === -1) await postListFormJsonRow(data.id, input)
|
||||||
else await putListFormJsonRow(data.id, input)
|
else await putListFormJsonRow(data.id, input)
|
||||||
toast.push(<Notification type="success">Kayıt kaydedildi</Notification>)
|
toast.push(
|
||||||
|
<Notification type="success">
|
||||||
|
{translate('::App.Platform.RecordSaved')}
|
||||||
|
</Notification>,
|
||||||
|
)
|
||||||
await handleClose()
|
await handleClose()
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
|
|
@ -92,19 +118,44 @@ function JsonRowOpDialogEditForm({
|
||||||
{({ errors, touched, isSubmitting }) => (
|
{({ errors, touched, isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<Dialog.Body className="grid grid-cols-2 gap-3">
|
<Dialog.Body className="grid grid-cols-2 gap-3">
|
||||||
<FormItem label={translate('::App.Listform.ListformField.Order')} invalid={!!(errors.order && touched.order)} errorMessage={errors.order}>
|
<FormItem
|
||||||
|
label={translate('::App.Listform.ListformField.Order')}
|
||||||
|
invalid={!!(errors.order && touched.order)}
|
||||||
|
errorMessage={errors.order}
|
||||||
|
>
|
||||||
<Field name="order" type="number" component={Input} />
|
<Field name="order" type="number" component={Input} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormItemType')}>
|
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormItemType')}>
|
||||||
<Field name="itemType">{({ field, form }: FieldProps) => <Select options={itemTypeOptions} value={itemTypeOptions.find((option) => option.value === field.value)} onChange={(option) => form.setFieldValue(field.name, option?.value)} />}</Field>
|
<Field name="itemType">
|
||||||
|
{({ field, form }: FieldProps) => (
|
||||||
|
<Select
|
||||||
|
options={itemTypeOptions}
|
||||||
|
value={itemTypeOptions.find((option) => option.value === field.value)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
className="col-span-2"
|
||||||
|
label={translate('::ListForms.ListFormEdit.DetailsTitle')}
|
||||||
|
>
|
||||||
|
<Field name="caption" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnCount')}>
|
||||||
|
<Field name="colCount" type="number" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnSpan')}>
|
||||||
|
<Field name="colSpan" type="number" component={Input} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem className="col-span-2" label={translate('::ListForms.ListFormEdit.DetailsTitle')}><Field name="caption" component={Input} /></FormItem>
|
|
||||||
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnCount')}><Field name="colCount" type="number" component={Input} /></FormItem>
|
|
||||||
<FormItem label={translate('::ListForms.ListFormEdit.EditingFormColumnSpan')}><Field name="colSpan" type="number" component={Input} /></FormItem>
|
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
<Dialog.Footer className="flex justify-end gap-2">
|
<Dialog.Footer className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="plain" onClick={handleClose}>{translate('::Cancel')}</Button>
|
<Button type="button" variant="plain" onClick={handleClose}>
|
||||||
<Button type="submit" variant="solid" loading={isSubmitting}>{translate('::Save')}</Button>
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="solid" loading={isSubmitting}>
|
||||||
|
{translate('::Save')}
|
||||||
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ function JsonRowOpDialogExtraFilter({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Alanlar getirilemedi
|
{translate('::App.Platform.FieldsFetchFailed')}
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
|
|
@ -156,7 +156,11 @@ function JsonRowOpDialogExtraFilter({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -164,7 +168,7 @@ function JsonRowOpDialogExtraFilter({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -286,7 +290,12 @@ function JsonRowOpDialogExtraFilter({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
@ -302,21 +311,26 @@ function JsonRowOpDialogExtraFilter({
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -331,7 +345,12 @@ function JsonRowOpDialogExtraFilter({
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,11 @@ function JsonRowOpDialogPane({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -102,7 +106,7 @@ function JsonRowOpDialogPane({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -134,7 +138,7 @@ function JsonRowOpDialogPane({
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="color"
|
type="color"
|
||||||
className='!h-10 !p-0 !m-0 !border-0'
|
className="!h-10 !p-0 !m-0 !border-0"
|
||||||
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -150,7 +154,12 @@ function JsonRowOpDialogPane({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
@ -166,21 +175,26 @@ function JsonRowOpDialogPane({
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete</h5>
|
<h5 className="mb-4">Delete</h5>
|
||||||
<p>Silmek istediğinize emin misiniz?</p>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
placement: 'bottom-end',
|
<Notification type="success">
|
||||||
})
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
},
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -194,7 +208,12 @@ function JsonRowOpDialogPane({
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<div className="text-right mt-4">
|
<div className="text-right mt-4">
|
||||||
<Button size="sm" className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
|
|
||||||
|
|
@ -135,358 +135,126 @@ function JsonRowOpDialogSeries({
|
||||||
>
|
>
|
||||||
{(data.operation === 'create' || data.operation === 'update') && (
|
{(data.operation === 'create' || data.operation === 'update') && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={
|
||||||
data.chartSeriesValues ?? {
|
data.chartSeriesValues ?? {
|
||||||
index: -1,
|
index: -1,
|
||||||
argumentField: '',
|
argumentField: '',
|
||||||
axis: '',
|
axis: '',
|
||||||
barOverlapGroup: '',
|
barOverlapGroup: '',
|
||||||
barPadding: 0,
|
barPadding: 0,
|
||||||
barWidth: 0,
|
barWidth: 0,
|
||||||
color: '',
|
color: '',
|
||||||
cornerRadius: 0,
|
cornerRadius: 0,
|
||||||
dashStyle: 'solid',
|
dashStyle: 'solid',
|
||||||
ignoreEmptyPoints: false,
|
ignoreEmptyPoints: false,
|
||||||
name: '',
|
name: '',
|
||||||
pane: '',
|
pane: '',
|
||||||
rangeValue1Field: '',
|
rangeValue1Field: '',
|
||||||
rangeValue2Field: '',
|
rangeValue2Field: '',
|
||||||
selectionMode: 'none',
|
selectionMode: 'none',
|
||||||
showInLegend: true,
|
showInLegend: true,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
valueField: '',
|
valueField: '',
|
||||||
|
visible: true,
|
||||||
|
width: 2,
|
||||||
|
label: {
|
||||||
visible: true,
|
visible: true,
|
||||||
width: 2,
|
backgroundColor: '#f05b41',
|
||||||
label: {
|
customizeText: '',
|
||||||
visible: true,
|
format: 'decimal',
|
||||||
backgroundColor: '#f05b41',
|
font: {
|
||||||
customizeText: '',
|
color: '#FFFFFF',
|
||||||
format: 'decimal',
|
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
|
||||||
font: {
|
size: 12,
|
||||||
color: '#FFFFFF',
|
weight: 400,
|
||||||
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
|
|
||||||
size: 12,
|
|
||||||
weight: 400,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
validationSchema={schema}
|
}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
validationSchema={schema}
|
||||||
setSubmitting(true)
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
try {
|
setSubmitting(true)
|
||||||
const input: ListFormJsonRowDto = {
|
try {
|
||||||
index: data.index,
|
const input: ListFormJsonRowDto = {
|
||||||
fieldName: data.tabName,
|
index: data.index,
|
||||||
itemChartSeries: values,
|
fieldName: data.tabName,
|
||||||
}
|
itemChartSeries: values,
|
||||||
if (data.index === -1) {
|
|
||||||
await postListFormJsonRow(data.id, input)
|
|
||||||
} else {
|
|
||||||
await putListFormJsonRow(data.id, input)
|
|
||||||
}
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success">
|
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.push(
|
|
||||||
<Notification type="danger">
|
|
||||||
Hata
|
|
||||||
<code>{error}</code>
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
}
|
||||||
}}
|
if (data.index === -1) {
|
||||||
>
|
await postListFormJsonRow(data.id, input)
|
||||||
{({ touched, errors, values, isSubmitting }) => (
|
} else {
|
||||||
<Form className="flex flex-col h-full">
|
await putListFormJsonRow(data.id, input)
|
||||||
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
|
}
|
||||||
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
|
toast.push(
|
||||||
<div className="flex-1 overflow-y-auto p-2">
|
<Notification type="success">
|
||||||
<div className="grid grid-cols-2 gap-6">
|
{translate(
|
||||||
{/* Sol Kolon */}
|
data.index === -1
|
||||||
<div className="flex flex-col gap-1">
|
? '::App.Platform.RecordAdded'
|
||||||
<FormItem
|
: '::App.Platform.RecordUpdated',
|
||||||
label="Visible"
|
)}
|
||||||
invalid={errors.visible && touched.visible}
|
</Notification>,
|
||||||
errorMessage={errors.visible}
|
{ placement: 'bottom-end' },
|
||||||
>
|
)
|
||||||
<Field name="visible" component={Checkbox} />
|
handleClose()
|
||||||
</FormItem>
|
} catch (error: any) {
|
||||||
|
toast.push(
|
||||||
<FormItem
|
<Notification type="danger">
|
||||||
label="Show In Legend"
|
{translate('::App.Platform.Error')}
|
||||||
invalid={errors.showInLegend && touched.showInLegend}
|
<code>{error}</code>
|
||||||
errorMessage={errors.showInLegend}
|
</Notification>,
|
||||||
>
|
{ placement: 'bottom-end' },
|
||||||
<Field name="showInLegend" component={Checkbox} />
|
)
|
||||||
</FormItem>
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
<FormItem
|
}
|
||||||
label="Ignore Empty Points"
|
}}
|
||||||
invalid={errors.ignoreEmptyPoints && touched.ignoreEmptyPoints}
|
>
|
||||||
errorMessage={errors.ignoreEmptyPoints}
|
{({ touched, errors, values, isSubmitting }) => (
|
||||||
>
|
<Form className="flex flex-col h-full">
|
||||||
<Field name="ignoreEmptyPoints" component={Checkbox} />
|
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
|
||||||
</FormItem>
|
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
|
||||||
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
<FormItem
|
<div className="grid grid-cols-2 gap-6">
|
||||||
label="Type"
|
{/* Sol Kolon */}
|
||||||
invalid={errors.type && touched.type}
|
<div className="flex flex-col gap-1">
|
||||||
errorMessage={errors.type}
|
<FormItem
|
||||||
>
|
label="Visible"
|
||||||
<Field type="text" name="type">
|
invalid={errors.visible && touched.visible}
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
errorMessage={errors.visible}
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={chartSeriesTypeOptions}
|
|
||||||
isClearable={true}
|
|
||||||
value={chartSeriesTypeOptions.filter(
|
|
||||||
(option) => option.value === values.type,
|
|
||||||
)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Name"
|
|
||||||
invalid={errors.name && touched.name}
|
|
||||||
errorMessage={errors.name}
|
|
||||||
>
|
|
||||||
<Field type="text" name="name" component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Argument Field"
|
|
||||||
invalid={errors.argumentField && touched.argumentField}
|
|
||||||
errorMessage={errors.argumentField}
|
|
||||||
>
|
|
||||||
<Field type="text" name="argumentField">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
isClearable={true}
|
|
||||||
options={fieldList}
|
|
||||||
value={fieldList?.find(
|
|
||||||
(option) => option.value === values.argumentField,
|
|
||||||
)}
|
|
||||||
menuPlacement="auto"
|
|
||||||
maxMenuHeight={150}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Value Field"
|
|
||||||
invalid={errors.valueField && touched.valueField}
|
|
||||||
errorMessage={errors.valueField}
|
|
||||||
>
|
|
||||||
<Field type="text" name="valueField">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
isClearable={true}
|
|
||||||
options={fieldList}
|
|
||||||
value={fieldList?.find((option) => option.value === values.valueField)}
|
|
||||||
menuPlacement="auto"
|
|
||||||
maxMenuHeight={150}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label={translate('::ListForms.ListFormFieldEdit.SummaryType')}
|
|
||||||
invalid={errors.summaryType && touched.summaryType}
|
|
||||||
errorMessage={errors.summaryType}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="summaryType"
|
|
||||||
placeholder={translate('::ListForms.ListFormFieldEdit.SummaryType')}
|
|
||||||
>
|
>
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
<Field name="visible" component={Checkbox} />
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
isClearable={true}
|
|
||||||
options={columnSummaryTypeListOptions}
|
|
||||||
value={columnSummaryTypeListOptions.filter(
|
|
||||||
(option: any) => option.value === values.summaryType,
|
|
||||||
)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Axis"
|
|
||||||
invalid={errors.axis && touched.axis}
|
|
||||||
errorMessage={errors.axis}
|
|
||||||
>
|
|
||||||
<Field type="text" name="axis">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
isClearable={true}
|
|
||||||
options={valueAxisList()}
|
|
||||||
value={valueAxisList()?.find((option) => option.value === values.axis)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Pane"
|
|
||||||
invalid={errors.pane && touched.pane}
|
|
||||||
errorMessage={errors.pane}
|
|
||||||
>
|
|
||||||
<Field type="text" name="pane">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
isClearable={true}
|
|
||||||
options={valuePaneList()}
|
|
||||||
value={valuePaneList()?.find((option) => option.value === values.pane)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Dash Style"
|
|
||||||
invalid={errors.dashStyle && touched.dashStyle}
|
|
||||||
errorMessage={errors.dashStyle}
|
|
||||||
>
|
|
||||||
<Field type="text" name="dashStyle">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={chartSeriesDashStyleOptions}
|
|
||||||
isClearable={true}
|
|
||||||
value={chartSeriesDashStyleOptions.filter(
|
|
||||||
(option) => option.value === values.dashStyle,
|
|
||||||
)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Color"
|
|
||||||
invalid={errors.color && touched.color}
|
|
||||||
errorMessage={errors.color}
|
|
||||||
>
|
|
||||||
<Field name="color">
|
|
||||||
{({ field, form }: FieldProps) => (
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="color"
|
|
||||||
className='!h-10 !p-0 !m-0 !border-0'
|
|
||||||
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Selection Mode"
|
|
||||||
invalid={errors.selectionMode && touched.selectionMode}
|
|
||||||
errorMessage={errors.selectionMode}
|
|
||||||
>
|
|
||||||
<Field type="text" name="selectionMode">
|
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={chartSeriesSelectionModeOptions}
|
|
||||||
isClearable={true}
|
|
||||||
value={chartSeriesSelectionModeOptions.filter(
|
|
||||||
(option) => option.value === values.selectionMode,
|
|
||||||
)}
|
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Width"
|
|
||||||
invalid={errors.width && touched.width}
|
|
||||||
errorMessage={errors.width}
|
|
||||||
>
|
|
||||||
<Field type="number" name="width" component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Corner Radius"
|
|
||||||
invalid={errors.cornerRadius && touched.cornerRadius}
|
|
||||||
errorMessage={errors.cornerRadius}
|
|
||||||
>
|
|
||||||
<Field type="number" name="cornerRadius" component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
{/* Sağ Kolon - Label */}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Card className="my-2" header="Label">
|
|
||||||
<FormItem label="Label Visible">
|
|
||||||
<Field name="label.visible" component={Checkbox} />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Background Color"
|
label="Show In Legend"
|
||||||
invalid={errors.label?.backgroundColor && touched.label?.backgroundColor}
|
invalid={errors.showInLegend && touched.showInLegend}
|
||||||
errorMessage={errors.label?.backgroundColor}
|
errorMessage={errors.showInLegend}
|
||||||
>
|
>
|
||||||
<Field name="label.backgroundColor">
|
<Field name="showInLegend" component={Checkbox} />
|
||||||
{({ field, form }: FieldProps) => (
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="color"
|
|
||||||
className='!h-10 !p-0 !m-0 !border-0'
|
|
||||||
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Format"
|
label="Ignore Empty Points"
|
||||||
invalid={errors.label?.format && touched.label?.format}
|
invalid={errors.ignoreEmptyPoints && touched.ignoreEmptyPoints}
|
||||||
errorMessage={errors.label?.format}
|
errorMessage={errors.ignoreEmptyPoints}
|
||||||
>
|
>
|
||||||
<Field type="text" name="label.format">
|
<Field name="ignoreEmptyPoints" component={Checkbox} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Type"
|
||||||
|
invalid={errors.type && touched.type}
|
||||||
|
errorMessage={errors.type}
|
||||||
|
>
|
||||||
|
<Field type="text" name="type">
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
<Select
|
<Select
|
||||||
field={field}
|
field={field}
|
||||||
form={form}
|
form={form}
|
||||||
options={tooltipFormatListOptions}
|
options={chartSeriesTypeOptions}
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
value={tooltipFormatListOptions.filter(
|
value={chartSeriesTypeOptions.filter(
|
||||||
(option) => option.value === values.label.format,
|
(option) => option.value === values.type,
|
||||||
)}
|
)}
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -495,97 +263,344 @@ function JsonRowOpDialogSeries({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Customize Text"
|
label="Name"
|
||||||
invalid={errors.label?.customizeText && touched.label?.customizeText}
|
invalid={errors.name && touched.name}
|
||||||
errorMessage={errors.label?.customizeText}
|
errorMessage={errors.name}
|
||||||
>
|
>
|
||||||
<Field type="text" name="label.customizeText" component={Input} />
|
<Field type="text" name="name" component={Input} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<Card className="my-2" header="Font">
|
<FormItem
|
||||||
<FormItem label="Color">
|
label="Argument Field"
|
||||||
<Field name="label.font.color">
|
invalid={errors.argumentField && touched.argumentField}
|
||||||
|
errorMessage={errors.argumentField}
|
||||||
|
>
|
||||||
|
<Field type="text" name="argumentField">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={fieldList}
|
||||||
|
value={fieldList?.find(
|
||||||
|
(option) => option.value === values.argumentField,
|
||||||
|
)}
|
||||||
|
menuPlacement="auto"
|
||||||
|
maxMenuHeight={150}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Value Field"
|
||||||
|
invalid={errors.valueField && touched.valueField}
|
||||||
|
errorMessage={errors.valueField}
|
||||||
|
>
|
||||||
|
<Field type="text" name="valueField">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={fieldList}
|
||||||
|
value={fieldList?.find(
|
||||||
|
(option) => option.value === values.valueField,
|
||||||
|
)}
|
||||||
|
menuPlacement="auto"
|
||||||
|
maxMenuHeight={150}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label={translate('::ListForms.ListFormFieldEdit.SummaryType')}
|
||||||
|
invalid={errors.summaryType && touched.summaryType}
|
||||||
|
errorMessage={errors.summaryType}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="summaryType"
|
||||||
|
placeholder={translate('::ListForms.ListFormFieldEdit.SummaryType')}
|
||||||
|
>
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={columnSummaryTypeListOptions}
|
||||||
|
value={columnSummaryTypeListOptions.filter(
|
||||||
|
(option: any) => option.value === values.summaryType,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Axis"
|
||||||
|
invalid={errors.axis && touched.axis}
|
||||||
|
errorMessage={errors.axis}
|
||||||
|
>
|
||||||
|
<Field type="text" name="axis">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={valueAxisList()}
|
||||||
|
value={valueAxisList()?.find(
|
||||||
|
(option) => option.value === values.axis,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Pane"
|
||||||
|
invalid={errors.pane && touched.pane}
|
||||||
|
errorMessage={errors.pane}
|
||||||
|
>
|
||||||
|
<Field type="text" name="pane">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={valuePaneList()}
|
||||||
|
value={valuePaneList()?.find(
|
||||||
|
(option) => option.value === values.pane,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Dash Style"
|
||||||
|
invalid={errors.dashStyle && touched.dashStyle}
|
||||||
|
errorMessage={errors.dashStyle}
|
||||||
|
>
|
||||||
|
<Field type="text" name="dashStyle">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
options={chartSeriesDashStyleOptions}
|
||||||
|
isClearable={true}
|
||||||
|
value={chartSeriesDashStyleOptions.filter(
|
||||||
|
(option) => option.value === values.dashStyle,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Color"
|
||||||
|
invalid={errors.color && touched.color}
|
||||||
|
errorMessage={errors.color}
|
||||||
|
>
|
||||||
|
<Field name="color">
|
||||||
|
{({ field, form }: FieldProps) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="color"
|
||||||
|
className="!h-10 !p-0 !m-0 !border-0"
|
||||||
|
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Selection Mode"
|
||||||
|
invalid={errors.selectionMode && touched.selectionMode}
|
||||||
|
errorMessage={errors.selectionMode}
|
||||||
|
>
|
||||||
|
<Field type="text" name="selectionMode">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
options={chartSeriesSelectionModeOptions}
|
||||||
|
isClearable={true}
|
||||||
|
value={chartSeriesSelectionModeOptions.filter(
|
||||||
|
(option) => option.value === values.selectionMode,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Width"
|
||||||
|
invalid={errors.width && touched.width}
|
||||||
|
errorMessage={errors.width}
|
||||||
|
>
|
||||||
|
<Field type="number" name="width" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Corner Radius"
|
||||||
|
invalid={errors.cornerRadius && touched.cornerRadius}
|
||||||
|
errorMessage={errors.cornerRadius}
|
||||||
|
>
|
||||||
|
<Field type="number" name="cornerRadius" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
{/* Sağ Kolon - Label */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Card className="my-2" header="Label">
|
||||||
|
<FormItem label="Label Visible">
|
||||||
|
<Field name="label.visible" component={Checkbox} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Background Color"
|
||||||
|
invalid={errors.label?.backgroundColor && touched.label?.backgroundColor}
|
||||||
|
errorMessage={errors.label?.backgroundColor}
|
||||||
|
>
|
||||||
|
<Field name="label.backgroundColor">
|
||||||
{({ field, form }: FieldProps) => (
|
{({ field, form }: FieldProps) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="color"
|
type="color"
|
||||||
className='!h-10 !p-0 !m-0 !border-0'
|
className="!h-10 !p-0 !m-0 !border-0"
|
||||||
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Family">
|
<FormItem
|
||||||
<Field name="label.font.family" component={Input} />
|
label="Format"
|
||||||
|
invalid={errors.label?.format && touched.label?.format}
|
||||||
|
errorMessage={errors.label?.format}
|
||||||
|
>
|
||||||
|
<Field type="text" name="label.format">
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
options={tooltipFormatListOptions}
|
||||||
|
isClearable={true}
|
||||||
|
value={tooltipFormatListOptions.filter(
|
||||||
|
(option) => option.value === values.label.format,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Size">
|
<FormItem
|
||||||
<Field type="number" name="label.font.size" component={Input} />
|
label="Customize Text"
|
||||||
|
invalid={errors.label?.customizeText && touched.label?.customizeText}
|
||||||
|
errorMessage={errors.label?.customizeText}
|
||||||
|
>
|
||||||
|
<Field type="text" name="label.customizeText" component={Input} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Weight">
|
<Card className="my-2" header="Font">
|
||||||
<Field type="number" name="label.font.weight" component={Input} />
|
<FormItem label="Color">
|
||||||
</FormItem>
|
<Field name="label.font.color">
|
||||||
|
{({ field, form }: FieldProps) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="color"
|
||||||
|
className="!h-10 !p-0 !m-0 !border-0"
|
||||||
|
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Family">
|
||||||
|
<Field name="label.font.family" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Size">
|
||||||
|
<Field type="number" name="label.font.size" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Weight">
|
||||||
|
<Field type="number" name="label.font.weight" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
</Card>
|
||||||
</Card>
|
</Card>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Body>
|
</div>
|
||||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
</Dialog.Body>
|
||||||
<Button size="sm" variant="plain" onClick={handleClose}>
|
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||||
{translate('::Cancel')}
|
<Button size="sm" variant="plain" onClick={handleClose}>
|
||||||
</Button>
|
{translate('::Cancel')}
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
</Button>
|
||||||
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
</Button>
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
||||||
</Dialog.Footer>
|
</Button>
|
||||||
</Form>
|
</Dialog.Footer>
|
||||||
)}
|
</Form>
|
||||||
</Formik>
|
)}
|
||||||
|
</Formik>
|
||||||
)}
|
)}
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
|
<Notification type="success">
|
||||||
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
})
|
},
|
||||||
handleClose()
|
)
|
||||||
} catch (error: any) {
|
handleClose()
|
||||||
toast.push(
|
} catch (error: any) {
|
||||||
<Notification type="danger">
|
toast.push(
|
||||||
Hata
|
<Notification type="danger">
|
||||||
<code>{error}</code>
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
<code>{error}</code>
|
||||||
{ placement: 'bottom-end' },
|
</Notification>,
|
||||||
)
|
{ placement: 'bottom-end' },
|
||||||
} finally {
|
)
|
||||||
setSubmitting(false)
|
} finally {
|
||||||
}
|
setSubmitting(false)
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
{({ isSubmitting }) => (
|
>
|
||||||
<Form className="flex flex-col h-full">
|
{({ isSubmitting }) => (
|
||||||
<Dialog.Body className="flex flex-col gap-2">
|
<Form className="flex flex-col h-full">
|
||||||
<h5>{translate('::Delete')}</h5>
|
<Dialog.Body className="flex flex-col gap-2">
|
||||||
<p>{translate('::DeleteConfirmation')}</p>
|
<h5>{translate('::Delete')}</h5>
|
||||||
</Dialog.Body>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
</Dialog.Body>
|
||||||
<Button size="sm" variant="plain" onClick={handleClose}>
|
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||||
{translate('::Cancel')}
|
<Button size="sm" variant="plain" onClick={handleClose}>
|
||||||
</Button>
|
{translate('::Cancel')}
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
</Button>
|
||||||
{isSubmitting ? 'Deleting' : 'Delete'}
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
</Button>
|
{isSubmitting ? 'Deleting' : 'Delete'}
|
||||||
</Dialog.Footer>
|
</Button>
|
||||||
</Form>
|
</Dialog.Footer>
|
||||||
)}
|
</Form>
|
||||||
</Formik>
|
)}
|
||||||
|
</Formik>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,11 @@ function JsonRowOpDialogSubForm({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success">
|
<Notification type="success">
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
{translate(
|
||||||
|
data.index === -1
|
||||||
|
? '::App.Platform.RecordAdded'
|
||||||
|
: '::App.Platform.RecordUpdated',
|
||||||
|
)}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -194,7 +198,7 @@ function JsonRowOpDialogSubForm({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
@ -451,12 +455,16 @@ function JsonRowOpDialogSubForm({
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>)
|
toast.push(
|
||||||
|
<Notification type="success">
|
||||||
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
<code>{error}</code>
|
<code>{error}</code>
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
|
|
|
||||||
|
|
@ -84,398 +84,403 @@ function JsonRowOpDialogWidget({
|
||||||
>
|
>
|
||||||
{(data.operation === 'create' || data.operation === 'update') && (
|
{(data.operation === 'create' || data.operation === 'update') && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={
|
||||||
data.widgetValues ?? {
|
data.widgetValues ?? {
|
||||||
colGap: 3,
|
colGap: 3,
|
||||||
colSpan: 3,
|
colSpan: 3,
|
||||||
sqlQuery:
|
sqlQuery:
|
||||||
"SELECT 'Total Records' as title, COUNT(*) as value, 'blue' as color, 'FaChartBar' as icon, 'Active records' as subTitle FROM YourTable WHERE IsActive = 1",
|
"SELECT 'Total Records' as title, COUNT(*) as value, 'blue' as color, 'FaChartBar' as icon, 'Active records' as subTitle FROM YourTable WHERE IsActive = 1",
|
||||||
className: 'mb-3',
|
className: 'mb-3',
|
||||||
valueClassName: 'bg-5 text-sm',
|
valueClassName: 'bg-5 text-sm',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
value: 'value',
|
value: 'value',
|
||||||
color: 'color',
|
color: 'color',
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
subTitle: 'subTitle',
|
subTitle: 'subTitle',
|
||||||
onClick: '',
|
onClick: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
validationSchema={schema}
|
}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
validationSchema={schema}
|
||||||
setSubmitting(true)
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
try {
|
setSubmitting(true)
|
||||||
const input: ListFormJsonRowDto = {
|
try {
|
||||||
index: data.index,
|
const input: ListFormJsonRowDto = {
|
||||||
fieldName: data.tabName,
|
index: data.index,
|
||||||
itemWidget: values,
|
fieldName: data.tabName,
|
||||||
}
|
itemWidget: values,
|
||||||
if (data.index === -1) {
|
|
||||||
await postListFormJsonRow(data.id, input)
|
|
||||||
} else {
|
|
||||||
await putListFormJsonRow(data.id, input)
|
|
||||||
}
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success">
|
|
||||||
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.push(
|
|
||||||
<Notification type="danger">
|
|
||||||
Hata
|
|
||||||
<code>{error}</code>
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
}
|
||||||
}}
|
if (data.index === -1) {
|
||||||
>
|
await postListFormJsonRow(data.id, input)
|
||||||
{({ touched, errors, values, isSubmitting, setFieldValue }) => (
|
} else {
|
||||||
<Form className="flex flex-col h-full">
|
await putListFormJsonRow(data.id, input)
|
||||||
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
|
}
|
||||||
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
|
toast.push(
|
||||||
<div className="flex-1 overflow-y-auto p-1">
|
<Notification type="success">
|
||||||
<div className="grid grid-cols-5 gap-4">
|
{translate(
|
||||||
<FormItem
|
data.index === -1
|
||||||
label="Column Gap"
|
? '::App.Platform.RecordAdded'
|
||||||
invalid={errors.colGap && touched.colGap}
|
: '::App.Platform.RecordUpdated',
|
||||||
errorMessage={errors.colGap}
|
)}
|
||||||
>
|
</Notification>,
|
||||||
<Field
|
{ placement: 'bottom-end' },
|
||||||
type="number"
|
)
|
||||||
autoComplete="off"
|
handleClose()
|
||||||
name="colGap"
|
} catch (error: any) {
|
||||||
placeholder="Column Gap"
|
toast.push(
|
||||||
component={Input}
|
<Notification type="danger">
|
||||||
/>
|
{translate('::App.Platform.Error')}
|
||||||
</FormItem>
|
<code>{error}</code>
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'bottom-end' },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ touched, errors, values, isSubmitting, setFieldValue }) => (
|
||||||
|
<Form className="flex flex-col h-full">
|
||||||
|
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
|
||||||
|
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
|
||||||
|
<div className="flex-1 overflow-y-auto p-1">
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
<FormItem
|
||||||
|
label="Column Gap"
|
||||||
|
invalid={errors.colGap && touched.colGap}
|
||||||
|
errorMessage={errors.colGap}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
autoComplete="off"
|
||||||
|
name="colGap"
|
||||||
|
placeholder="Column Gap"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Column Span"
|
label="Column Span"
|
||||||
invalid={errors.colSpan && touched.colSpan}
|
invalid={errors.colSpan && touched.colSpan}
|
||||||
errorMessage={errors.colSpan}
|
errorMessage={errors.colSpan}
|
||||||
>
|
>
|
||||||
<Field type="text" autoComplete="off" name="colSpan" placeholder="colSpan">
|
<Field type="text" autoComplete="off" name="colSpan" placeholder="colSpan">
|
||||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
<Select
|
<Select
|
||||||
field={field}
|
field={field}
|
||||||
form={form}
|
form={form}
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
options={colSpanOptions}
|
options={colSpanOptions}
|
||||||
value={colSpanOptions?.filter(
|
value={colSpanOptions?.filter(
|
||||||
(option: any) => option.value === values.colSpan,
|
(option: any) => option.value === values.colSpan,
|
||||||
)}
|
)}
|
||||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div className="text-xs">
|
|
||||||
<div className="font-semibold mb-1">Widget Container CSS Classes</div>
|
|
||||||
<div>Examples: mb-3, mt-2, p-4, rounded-lg, shadow-md</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
label="Class Name"
|
|
||||||
invalid={errors.className && touched.className}
|
|
||||||
errorMessage={errors.className}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="className"
|
|
||||||
placeholder="Tailwind CSS classes: mb-3, mt-2, p-4..."
|
|
||||||
component={Input}
|
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
)}
|
||||||
</Tooltip>
|
</Field>
|
||||||
|
</FormItem>
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div className="text-xs">
|
|
||||||
<div className="font-semibold mb-1">Value Display CSS Classes</div>
|
|
||||||
<div>Examples: text-3xl, text-2xl, font-bold, text-sm, text-center</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
label="Value Class Name"
|
|
||||||
invalid={errors.valueClassName && touched.valueClassName}
|
|
||||||
errorMessage={errors.valueClassName}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="valueClassName"
|
|
||||||
placeholder="Tailwind CSS classes: text-3xl, font-bold..."
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="IsActive"
|
|
||||||
invalid={errors.isActive && touched.isActive}
|
|
||||||
errorMessage={errors.isActive}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
autoComplete="off"
|
|
||||||
name="isActive"
|
|
||||||
placeholder="IsActive"
|
|
||||||
component={Checkbox}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<div className="text-xs max-h-96 overflow-y-auto">
|
<div className="text-xs">
|
||||||
<div className="font-semibold mb-2">SQL Query Examples:</div>
|
<div className="font-semibold mb-1">Widget Container CSS Classes</div>
|
||||||
<div className="space-y-3">
|
<div>Examples: mb-3, mt-2, p-4, rounded-lg, shadow-md</div>
|
||||||
<div>
|
|
||||||
<code className="text-xs bg-gray-800 p-1 rounded block mt-1">
|
|
||||||
{
|
|
||||||
"SELECT 'Aktif' as title, COUNT(Id) as value, 'blue' as color, 'FaChartBar' as icon, 'Aktif kayıtlar' as subTitle FROM YourTable WHERE IsActive = 1"
|
|
||||||
}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Sql Query
|
<FormItem
|
||||||
|
label="Class Name"
|
||||||
|
invalid={errors.className && touched.className}
|
||||||
|
errorMessage={errors.className}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="className"
|
||||||
|
placeholder="Tailwind CSS classes: mb-3, mt-2, p-4..."
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div className="text-xs">
|
||||||
|
<div className="font-semibold mb-1">Value Display CSS Classes</div>
|
||||||
|
<div>Examples: text-3xl, text-2xl, font-bold, text-sm, text-center</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
label="Value Class Name"
|
||||||
|
invalid={errors.valueClassName && touched.valueClassName}
|
||||||
|
errorMessage={errors.valueClassName}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="valueClassName"
|
||||||
|
placeholder="Tailwind CSS classes: text-3xl, font-bold..."
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
invalid={errors.sqlQuery && touched.sqlQuery}
|
label="IsActive"
|
||||||
errorMessage={errors.sqlQuery}
|
invalid={errors.isActive && touched.isActive}
|
||||||
|
errorMessage={errors.isActive}
|
||||||
>
|
>
|
||||||
<div
|
<Field
|
||||||
className="border rounded-lg overflow-hidden"
|
autoComplete="off"
|
||||||
style={{ height: '35vh' }}
|
name="isActive"
|
||||||
>
|
placeholder="IsActive"
|
||||||
<SqlEditor
|
component={Checkbox}
|
||||||
value={values.sqlQuery || ''}
|
/>
|
||||||
height="35vh"
|
|
||||||
onChange={(value) => setFieldValue('sqlQuery', value || '')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<Tooltip
|
||||||
<FormItem
|
title={
|
||||||
label="Title Field"
|
<div className="text-xs max-h-96 overflow-y-auto">
|
||||||
invalid={errors.title && touched.title}
|
<div className="font-semibold mb-2">SQL Query Examples:</div>
|
||||||
errorMessage={errors.title}
|
<div className="space-y-3">
|
||||||
>
|
<div>
|
||||||
<Field
|
<code className="text-xs bg-gray-800 p-1 rounded block mt-1">
|
||||||
type="text"
|
{
|
||||||
autoComplete="off"
|
"SELECT 'Aktif' as title, COUNT(Id) as value, 'blue' as color, 'FaChartBar' as icon, 'Aktif kayıtlar' as subTitle FROM YourTable WHERE IsActive = 1"
|
||||||
name="title"
|
}
|
||||||
placeholder="Column name from SQL query e.g., 'title'"
|
</code>
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Value Field"
|
|
||||||
invalid={errors.value && touched.value}
|
|
||||||
errorMessage={errors.value}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="value"
|
|
||||||
placeholder="Column name from SQL query e.g., 'value'"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div className="text-xs">
|
|
||||||
<div className="font-semibold mb-1">Available Colors:</div>
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<div>• blue</div>
|
|
||||||
<div>• green</div>
|
|
||||||
<div>• purple</div>
|
|
||||||
<div>• gray</div>
|
|
||||||
<div>• red</div>
|
|
||||||
<div>• yellow</div>
|
|
||||||
<div>• pink</div>
|
|
||||||
<div>• indigo</div>
|
|
||||||
<div>• teal</div>
|
|
||||||
<div>• orange</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
>
|
</div>
|
||||||
<FormItem
|
}
|
||||||
label="Color Field"
|
>
|
||||||
invalid={errors.color && touched.color}
|
Sql Query
|
||||||
errorMessage={errors.color}
|
</Tooltip>
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="color"
|
|
||||||
placeholder="blue, green, purple, gray, red..."
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip
|
<FormItem
|
||||||
title={
|
invalid={errors.sqlQuery && touched.sqlQuery}
|
||||||
<div className="text-xs max-h-64 overflow-y-auto">
|
errorMessage={errors.sqlQuery}
|
||||||
<div className="font-semibold mb-2">Popular Icon Examples:</div>
|
>
|
||||||
<div className="space-y-1">
|
<div className="border rounded-lg overflow-hidden" style={{ height: '35vh' }}>
|
||||||
<div>
|
<SqlEditor
|
||||||
📊 <strong>Charts:</strong> FaChartBar, FaChartLine, FaChartPie,
|
value={values.sqlQuery || ''}
|
||||||
FaChartArea
|
height="35vh"
|
||||||
</div>
|
onChange={(value) => setFieldValue('sqlQuery', value || '')}
|
||||||
<div>
|
/>
|
||||||
💰 <strong>Finance:</strong> FaDollarSign, FaMoneyBill, FaWallet,
|
|
||||||
FaCreditCard
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
👥 <strong>Users:</strong> FaUser, FaUsers, FaUserCircle, FaUserTie
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
📦 <strong>Business:</strong> FaShoppingCart, FaBoxes, FaWarehouse,
|
|
||||||
FaTruck
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown,
|
|
||||||
FaArrowTrendUp
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
⚙️ <strong>Settings:</strong> FaCog, FaTools, FaWrench
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
🔔 <strong>Alerts:</strong> FaBell, FaExclamation, FaInfoCircle
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
📁 <strong>Files:</strong> FaFile, FaFolder, FaFileAlt, FaDownload
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
🏠 <strong>Other:</strong> FaHome, FaBuilding, FaGlobe, FaHeart
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 pt-2 border-t">
|
|
||||||
All icons from <strong>react-icons/fa</strong> (Font Awesome)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
label="Icon Field"
|
|
||||||
invalid={errors.icon && touched.icon}
|
|
||||||
errorMessage={errors.icon}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="icon"
|
|
||||||
placeholder="FaChartBar, FaUsers, FaShoppingCart..."
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Sub Title Field"
|
|
||||||
invalid={errors.subTitle && touched.subTitle}
|
|
||||||
errorMessage={errors.subTitle}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="subTitle"
|
|
||||||
placeholder="Column name from SQL query e.g., 'subTitle'"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
<FormItem
|
<FormItem
|
||||||
label="On Click"
|
label="Title Field"
|
||||||
invalid={errors.onClick && touched.onClick}
|
invalid={errors.title && touched.title}
|
||||||
errorMessage={errors.onClick}
|
errorMessage={errors.title}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="onClick"
|
name="title"
|
||||||
placeholder="JavaScript function: () => alert('Clicked')"
|
placeholder="Column name from SQL query e.g., 'title'"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Value Field"
|
||||||
|
invalid={errors.value && touched.value}
|
||||||
|
errorMessage={errors.value}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="value"
|
||||||
|
placeholder="Column name from SQL query e.g., 'value'"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div className="text-xs">
|
||||||
|
<div className="font-semibold mb-1">Available Colors:</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
<div>• blue</div>
|
||||||
|
<div>• green</div>
|
||||||
|
<div>• purple</div>
|
||||||
|
<div>• gray</div>
|
||||||
|
<div>• red</div>
|
||||||
|
<div>• yellow</div>
|
||||||
|
<div>• pink</div>
|
||||||
|
<div>• indigo</div>
|
||||||
|
<div>• teal</div>
|
||||||
|
<div>• orange</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
label="Color Field"
|
||||||
|
invalid={errors.color && touched.color}
|
||||||
|
errorMessage={errors.color}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="color"
|
||||||
|
placeholder="blue, green, purple, gray, red..."
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div className="text-xs max-h-64 overflow-y-auto">
|
||||||
|
<div className="font-semibold mb-2">Popular Icon Examples:</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>
|
||||||
|
📊 <strong>Charts:</strong> FaChartBar, FaChartLine, FaChartPie,
|
||||||
|
FaChartArea
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
💰 <strong>Finance:</strong> FaDollarSign, FaMoneyBill, FaWallet,
|
||||||
|
FaCreditCard
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
👥 <strong>Users:</strong> FaUser, FaUsers, FaUserCircle, FaUserTie
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
📦 <strong>Business:</strong> FaShoppingCart, FaBoxes, FaWarehouse,
|
||||||
|
FaTruck
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
📈 <strong>Analytics:</strong> FaArrowUp, FaArrowDown, FaArrowTrendUp
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
⚙️ <strong>Settings:</strong> FaCog, FaTools, FaWrench
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
🔔 <strong>Alerts:</strong> FaBell, FaExclamation, FaInfoCircle
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
📁 <strong>Files:</strong> FaFile, FaFolder, FaFileAlt, FaDownload
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
🏠 <strong>Other:</strong> FaHome, FaBuilding, FaGlobe, FaHeart
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 pt-2 border-t">
|
||||||
|
All icons from <strong>react-icons/fa</strong> (Font Awesome)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
label="Icon Field"
|
||||||
|
invalid={errors.icon && touched.icon}
|
||||||
|
errorMessage={errors.icon}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="icon"
|
||||||
|
placeholder="FaChartBar, FaUsers, FaShoppingCart..."
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Sub Title Field"
|
||||||
|
invalid={errors.subTitle && touched.subTitle}
|
||||||
|
errorMessage={errors.subTitle}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="subTitle"
|
||||||
|
placeholder="Column name from SQL query e.g., 'subTitle'"
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
<FormItem
|
||||||
<Button size="sm" variant="plain" onClick={handleClose}>
|
label="On Click"
|
||||||
{translate('::Cancel')}
|
invalid={errors.onClick && touched.onClick}
|
||||||
</Button>
|
errorMessage={errors.onClick}
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
>
|
||||||
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
<Field
|
||||||
</Button>
|
type="text"
|
||||||
</Dialog.Footer>
|
autoComplete="off"
|
||||||
</Form>
|
name="onClick"
|
||||||
)}
|
placeholder="JavaScript function: () => alert('Clicked')"
|
||||||
</Formik>
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||||
|
<Button size="sm" variant="plain" onClick={handleClose}>
|
||||||
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
)}
|
)}
|
||||||
{data.operation === 'delete' && (
|
{data.operation === 'delete' && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={data}
|
initialValues={data}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
await deleteListFormJsonRow(data.id, data.tabName, values.index)
|
||||||
toast.push(<Notification type="success">Kayıt silindi </Notification>, {
|
toast.push(
|
||||||
|
<Notification type="success">
|
||||||
|
{translate('::App.Platform.RecordDeleted')}
|
||||||
|
</Notification>,
|
||||||
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
})
|
},
|
||||||
handleClose()
|
)
|
||||||
} catch (error: any) {
|
handleClose()
|
||||||
toast.push(
|
} catch (error: any) {
|
||||||
<Notification type="danger">
|
toast.push(
|
||||||
Hata
|
<Notification type="danger">
|
||||||
<code>{error}</code>
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
<code>{error}</code>
|
||||||
{ placement: 'bottom-end' },
|
</Notification>,
|
||||||
)
|
{ placement: 'bottom-end' },
|
||||||
} finally {
|
)
|
||||||
setSubmitting(false)
|
} finally {
|
||||||
}
|
setSubmitting(false)
|
||||||
// getListFormJsonRow()
|
}
|
||||||
}}
|
// getListFormJsonRow()
|
||||||
>
|
}}
|
||||||
{({ isSubmitting }) => (
|
>
|
||||||
<Form className="flex flex-col h-full">
|
{({ isSubmitting }) => (
|
||||||
<Dialog.Body className="flex flex-col gap-2">
|
<Form className="flex flex-col h-full">
|
||||||
<h5>{translate('::Delete')}</h5>
|
<Dialog.Body className="flex flex-col gap-2">
|
||||||
<p>{translate('::DeleteConfirmation')}</p>
|
<h5>{translate('::Delete')}</h5>
|
||||||
</Dialog.Body>
|
<p>{translate('::DeleteConfirmation')}</p>
|
||||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
</Dialog.Body>
|
||||||
<Button size="sm" variant="plain" onClick={handleClose}>
|
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||||
{translate('::Cancel')}
|
<Button size="sm" variant="plain" onClick={handleClose}>
|
||||||
</Button>
|
{translate('::Cancel')}
|
||||||
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
</Button>
|
||||||
{isSubmitting ? 'Deleting' : 'Delete'}
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
||||||
</Button>
|
{isSubmitting ? 'Deleting' : 'Delete'}
|
||||||
</Dialog.Footer>
|
</Button>
|
||||||
</Form>
|
</Dialog.Footer>
|
||||||
)}
|
</Form>
|
||||||
</Formik>
|
)}
|
||||||
|
</Formik>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||||
import { FaExclamationTriangle, FaUndo } from 'react-icons/fa'
|
import { FaExclamationTriangle, FaUndo } from 'react-icons/fa'
|
||||||
import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
|
import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
|
||||||
import type { OptionSpec } from './optionSpecs'
|
import type { OptionSpec } from './optionSpecs'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
export const controlClass =
|
export const controlClass =
|
||||||
'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60'
|
'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60'
|
||||||
|
|
@ -15,29 +16,52 @@ type OptionFieldProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Değerin spec ile uyuşup uyuşmadığını söyler; uyuşmuyorsa alan kilitlenir. */
|
/** Değerin spec ile uyuşup uyuşmadığını söyler; uyuşmuyorsa alan kilitlenir. */
|
||||||
const detectMismatch = (spec: OptionSpec, value: unknown): string | undefined => {
|
const detectMismatch = (
|
||||||
|
spec: OptionSpec,
|
||||||
|
value: unknown,
|
||||||
|
): { key: string; params?: Record<string, string> } | undefined => {
|
||||||
if (value === undefined || value === null) return undefined
|
if (value === undefined || value === null) return undefined
|
||||||
|
|
||||||
const isObjectLike = typeof value === 'object'
|
const isObjectLike = typeof value === 'object'
|
||||||
|
|
||||||
if (spec.type === 'json') return undefined
|
if (spec.type === 'json') return undefined
|
||||||
if (spec.type === 'stringList') {
|
if (spec.type === 'stringList') {
|
||||||
return Array.isArray(value) ? undefined : 'Bu alan dizi bekliyor, mevcut değer dizi değil.'
|
return Array.isArray(value)
|
||||||
|
? undefined
|
||||||
|
: { key: '::ListForms.EditorOptions.Mismatch.ExpectedArray' }
|
||||||
}
|
}
|
||||||
if (isObjectLike) {
|
if (isObjectLike) {
|
||||||
return 'Bu yol şu anda nesne/dizi değeri tutuyor. Ham JSON sekmesinden düzenleyin.'
|
return { key: '::ListForms.EditorOptions.Mismatch.ObjectValue' }
|
||||||
}
|
}
|
||||||
if (spec.type === 'number' && typeof value !== 'number') {
|
if (spec.type === 'number' && typeof value !== 'number') {
|
||||||
return `Sayı bekleniyor, mevcut değer ${typeof value}. Yeni değer girildiğinde sayıya çevrilir.`
|
return {
|
||||||
|
key: '::ListForms.EditorOptions.Mismatch.ExpectedNumber',
|
||||||
|
params: { type: typeof value },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (spec.type === 'boolean' && typeof value !== 'boolean') {
|
if (spec.type === 'boolean' && typeof value !== 'boolean') {
|
||||||
return `Boolean bekleniyor, mevcut değer ${typeof value}. Listeden seçim yaparsan düzelir.`
|
return {
|
||||||
|
key: '::ListForms.EditorOptions.Mismatch.ExpectedBoolean',
|
||||||
|
params: { type: typeof value },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
|
const BooleanControl = ({
|
||||||
const current = value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : ''
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: unknown
|
||||||
|
onChange: (v: unknown) => void
|
||||||
|
}) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const current =
|
||||||
|
value === true || value === 'true'
|
||||||
|
? 'true'
|
||||||
|
: value === false || value === 'false'
|
||||||
|
? 'false'
|
||||||
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
|
|
@ -48,7 +72,7 @@ const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unk
|
||||||
onChange(event.target.value === 'true')
|
onChange(event.target.value === 'true')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">— tanımsız —</option>
|
<option value="">{translate('::ListForms.EditorOptions.Undefined')}</option>
|
||||||
<option value="true">true</option>
|
<option value="true">true</option>
|
||||||
<option value="false">false</option>
|
<option value="false">false</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -64,6 +88,7 @@ const SelectControl = ({
|
||||||
value: unknown
|
value: unknown
|
||||||
onChange: (v: unknown) => void
|
onChange: (v: unknown) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const current = value === undefined || value === null ? '' : String(value)
|
const current = value === undefined || value === null ? '' : String(value)
|
||||||
const known = spec.choices?.some((choice) => String(choice.value) === current)
|
const known = spec.choices?.some((choice) => String(choice.value) === current)
|
||||||
|
|
||||||
|
|
@ -78,18 +103,23 @@ const SelectControl = ({
|
||||||
onChange(choice ? choice.value : raw)
|
onChange(choice ? choice.value : raw)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">— tanımsız —</option>
|
<option value="">{translate('::ListForms.EditorOptions.Undefined')}</option>
|
||||||
{spec.choices?.map((choice) => (
|
{spec.choices?.map((choice) => (
|
||||||
<option key={String(choice.value)} value={String(choice.value)}>
|
<option key={String(choice.value)} value={String(choice.value)}>
|
||||||
{choice.label}
|
{choice.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{current && !known && <option value={current}>{current} (mevcut değer)</option>}
|
{current && !known && (
|
||||||
|
<option value={current}>
|
||||||
|
{current} ({translate('::ListForms.EditorOptions.CurrentValue')})
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
|
const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const serialized = value === undefined ? '' : JSON.stringify(value, null, 2)
|
const serialized = value === undefined ? '' : JSON.stringify(value, null, 2)
|
||||||
const [draft, setDraft] = useState(serialized)
|
const [draft, setDraft] = useState(serialized)
|
||||||
const [invalid, setInvalid] = useState(false)
|
const [invalid, setInvalid] = useState(false)
|
||||||
|
|
@ -122,7 +152,11 @@ const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknow
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{invalid && <p className="mt-1 text-[11px] text-red-500">Geçersiz JSON, değer kaydedilmedi.</p>}
|
{invalid && (
|
||||||
|
<p className="mt-1 text-[11px] text-red-500">
|
||||||
|
{translate('::ListForms.EditorOptions.InvalidJsonNotSaved')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -135,23 +169,30 @@ const StringListControl = ({
|
||||||
spec: OptionSpec
|
spec: OptionSpec
|
||||||
value: unknown
|
value: unknown
|
||||||
onChange: (v: unknown) => void
|
onChange: (v: unknown) => void
|
||||||
}) => (
|
}) => {
|
||||||
<input
|
const { translate } = useLocalization()
|
||||||
className={controlClass}
|
|
||||||
value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
|
return (
|
||||||
placeholder={spec.placeholder}
|
<input
|
||||||
onChange={(event) => {
|
className={controlClass}
|
||||||
const items = event.target.value
|
value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
|
||||||
.split(',')
|
placeholder={spec.placeholder ? translate('::' + spec.placeholder) : undefined}
|
||||||
.map((item) => item.trim())
|
onChange={(event) => {
|
||||||
.filter(Boolean)
|
const items = event.target.value
|
||||||
onChange(items.length ? items : undefined)
|
.split(',')
|
||||||
}}
|
.map((item) => item.trim())
|
||||||
/>
|
.filter(Boolean)
|
||||||
)
|
onChange(items.length ? items : undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const mismatch = detectMismatch(spec, value)
|
const mismatch = detectMismatch(spec, value)
|
||||||
|
const help = spec.help ? translate('::' + spec.help) : undefined
|
||||||
|
const placeholder = spec.placeholder ? translate('::' + spec.placeholder) : undefined
|
||||||
const isSet = value !== undefined
|
const isSet = value !== undefined
|
||||||
// Nesne/dizi tutan bir yol basit girdiyle düzenlenemez; ham JSON'a yönlendiriyoruz.
|
// Nesne/dizi tutan bir yol basit girdiyle düzenlenemez; ham JSON'a yönlendiriyoruz.
|
||||||
// Basit tip uyuşmazlıkları (metin yazılmış sayı gibi) düzenlenebilir kalır.
|
// Basit tip uyuşmazlıkları (metin yazılmış sayı gibi) düzenlenebilir kalır.
|
||||||
|
|
@ -177,7 +218,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
type="number"
|
type="number"
|
||||||
value={typeof value === 'number' ? value : leafToText(value)}
|
value={typeof value === 'number' ? value : leafToText(value)}
|
||||||
placeholder={spec.placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(event) => onChange(coerceNumber(event.target.value))}
|
onChange={(event) => onChange(coerceNumber(event.target.value))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -186,7 +227,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
<input
|
<input
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={leafToText(value)}
|
value={leafToText(value)}
|
||||||
placeholder={spec.placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(event) => onChange(coerceSize(event.target.value))}
|
onChange={(event) => onChange(coerceSize(event.target.value))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -195,8 +236,10 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
<input
|
<input
|
||||||
className={controlClass}
|
className={controlClass}
|
||||||
value={leafToText(value)}
|
value={leafToText(value)}
|
||||||
placeholder={spec.placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(event) => onChange(event.target.value === '' ? undefined : event.target.value)}
|
onChange={(event) =>
|
||||||
|
onChange(event.target.value === '' ? undefined : event.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -213,14 +256,14 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
<div className="mb-1 flex items-center gap-1">
|
<div className="mb-1 flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
className={`min-w-0 truncate text-xs ${isSet ? 'font-semibold text-indigo-600 dark:text-indigo-300' : 'text-gray-500'}`}
|
className={`min-w-0 truncate text-xs ${isSet ? 'font-semibold text-indigo-600 dark:text-indigo-300' : 'text-gray-500'}`}
|
||||||
title={spec.help ? `${spec.path} — ${spec.help}` : spec.path}
|
title={help ? `${spec.path} — ${help}` : spec.path}
|
||||||
>
|
>
|
||||||
{spec.label}
|
{spec.label}
|
||||||
</span>
|
</span>
|
||||||
{spec.platform && (
|
{spec.platform && (
|
||||||
<span
|
<span
|
||||||
className="rounded bg-emerald-100 px-1 text-[10px] text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200"
|
className="rounded bg-emerald-100 px-1 text-[10px] text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200"
|
||||||
title="Bu alan backend tarafından tipli olarak okunur; yanlış tipte yazılırsa yok sayılır."
|
title={translate('::ListForms.EditorOptions.PlatformFieldHint')}
|
||||||
>
|
>
|
||||||
platform
|
platform
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -229,7 +272,7 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto shrink-0 text-[10px] text-gray-400 hover:text-red-500"
|
className="ml-auto shrink-0 text-[10px] text-gray-400 hover:text-red-500"
|
||||||
title="Bu ayarı JSON'dan kaldır"
|
title={translate('::ListForms.EditorOptions.RemoveFromJson')}
|
||||||
onClick={() => onChange(undefined)}
|
onClick={() => onChange(undefined)}
|
||||||
>
|
>
|
||||||
<FaUndo />
|
<FaUndo />
|
||||||
|
|
@ -240,13 +283,13 @@ const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
|
||||||
{mismatch && (
|
{mismatch && (
|
||||||
<p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600">
|
<p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600">
|
||||||
<FaExclamationTriangle className="mt-[2px] shrink-0" />
|
<FaExclamationTriangle className="mt-[2px] shrink-0" />
|
||||||
<span>{mismatch}</span>
|
<span>{translate(mismatch.key, mismatch.params)}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */}
|
{/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */}
|
||||||
{!mismatch && spec.help && (
|
{!mismatch && help && (
|
||||||
<p className="mt-1 truncate text-[11px] text-gray-400" title={spec.help}>
|
<p className="mt-1 truncate text-[11px] text-gray-400" title={help}>
|
||||||
{spec.help}
|
{help}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const parseOptionsJson = (value?: string): ParseResult => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed)
|
const parsed = JSON.parse(trimmed)
|
||||||
if (!isPlainObject(parsed)) {
|
if (!isPlainObject(parsed)) {
|
||||||
return { data: {}, error: 'editorOptions bir JSON nesnesi ({ ... }) olmalıdır.' }
|
return { data: {}, error: 'ListForms.EditorOptions.MustBeJsonObject' }
|
||||||
}
|
}
|
||||||
return { data: parsed }
|
return { data: parsed }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -57,41 +57,68 @@ export type OptionGroupKey =
|
||||||
export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [
|
export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [
|
||||||
{
|
{
|
||||||
key: 'common',
|
key: 'common',
|
||||||
title: 'Genel Davranış',
|
title: 'ListForms.EditorOptions.Spec.Common.Title',
|
||||||
description: 'Tüm editörlerde geçerli olan durum, yetki ve doğrulama ayarları.',
|
description: 'ListForms.EditorOptions.Spec.Common.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'appearance',
|
key: 'appearance',
|
||||||
title: 'Görünüm ve Boyut',
|
title: 'ListForms.EditorOptions.Spec.Appearance.Title',
|
||||||
description: 'Etiket, boyut ve stil ayarları. Boyutlar sayı veya 100% gibi CSS değeri olabilir.',
|
description: 'ListForms.EditorOptions.Spec.Appearance.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'text',
|
||||||
|
title: 'Metin / Maske',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Text.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'number',
|
||||||
|
title: 'ListForms.EditorOptions.Spec.Number.Title',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Number.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'date',
|
||||||
|
title: 'ListForms.EditorOptions.Spec.Date.Title',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Date.Description',
|
||||||
},
|
},
|
||||||
{ key: 'text', title: 'Metin / Maske', description: 'TextBox, TextArea ve Autocomplete ayarları.' },
|
|
||||||
{ key: 'number', title: 'Sayı', description: 'NumberBox için sınır, adım ve format ayarları.' },
|
|
||||||
{ key: 'date', title: 'Tarih / Saat', description: 'DateBox, Calendar ve DateRangeBox ayarları.' },
|
|
||||||
{
|
{
|
||||||
key: 'dropdown',
|
key: 'dropdown',
|
||||||
title: 'Açılır Liste / Arama',
|
title: 'ListForms.EditorOptions.Spec.Dropdown.Title',
|
||||||
description: 'SelectBox, Lookup, TagBox ve GridBox için ortak liste ve arama ayarları.',
|
description: 'ListForms.EditorOptions.Spec.Dropdown.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tagBox',
|
||||||
|
title: 'TagBox',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.TagBox.Description',
|
||||||
},
|
},
|
||||||
{ key: 'tagBox', title: 'TagBox', description: 'Çoklu seçim editörüne özel ayarlar.' },
|
|
||||||
{
|
{
|
||||||
key: 'gridBox',
|
key: 'gridBox',
|
||||||
title: 'GridBox',
|
title: 'GridBox',
|
||||||
description: 'Tablo görünümlü açılır seçici. Bu alanlar backend tarafından GridBoxOptions olarak okunur.',
|
description: 'ListForms.EditorOptions.Spec.GridBox.Description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'image',
|
key: 'image',
|
||||||
title: 'Görsel Yükleme / Önizleme',
|
title: 'ListForms.EditorOptions.Spec.Image.Title',
|
||||||
description: 'ImageUpload ve ImageViewer editörleri için yükleme ve küçük resim ayarları.',
|
description: 'ListForms.EditorOptions.Spec.Image.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'choice',
|
||||||
|
title: 'ListForms.EditorOptions.Spec.Choice.Title',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Choice.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'slider',
|
||||||
|
title: 'ListForms.EditorOptions.Spec.Slider.Title',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Slider.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'html',
|
||||||
|
title: 'ListForms.EditorOptions.Spec.Html.Title',
|
||||||
|
description: 'ListForms.EditorOptions.Spec.Html.Description',
|
||||||
},
|
},
|
||||||
{ key: 'choice', title: 'Onay / Seçim', description: 'CheckBox, Switch, RadioGroup ve ColorBox ayarları.' },
|
|
||||||
{ key: 'slider', title: 'Kaydırıcı', description: 'Slider ve RangeSlider ayarları.' },
|
|
||||||
{ key: 'html', title: 'HTML Editör', description: 'dxHtmlEditor araç çubuğu ve görsel ayarları.' },
|
|
||||||
{
|
{
|
||||||
key: 'grid',
|
key: 'grid',
|
||||||
title: 'Liste Sütunu Etkisi',
|
title: 'ListForms.EditorOptions.Spec.Grid.Title',
|
||||||
description:
|
description: 'ListForms.EditorOptions.Spec.Grid.Description',
|
||||||
'Bu alanlar düzenleme editörünün yanı sıra listedeki sütun gösterimini de etkiler.',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -115,8 +142,6 @@ const SLIDER_EDITORS = ['dxSlider', 'dxRangeSlider']
|
||||||
/** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */
|
/** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */
|
||||||
const NON_INPUT_EDITORS = ['dxCheckBox', 'dxSwitch', 'dxRadioGroup', ...SLIDER_EDITORS]
|
const NON_INPUT_EDITORS = ['dxCheckBox', 'dxSwitch', 'dxRadioGroup', ...SLIDER_EDITORS]
|
||||||
|
|
||||||
const booleanHelp = 'Boş bırakılırsa JSON\'a yazılmaz ve DevExtreme varsayılanı geçerli olur.'
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ------------------------------------------------------------------ *
|
||||||
* Ayar sözlüğü
|
* Ayar sözlüğü
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
@ -128,31 +153,43 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
label: 'readOnly',
|
label: 'readOnly',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
help: 'Alan görünür ama değiştirilemez. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.ReadOnly.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'disabled',
|
path: 'disabled',
|
||||||
label: 'disabled',
|
label: 'disabled',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
help: 'Alan pasifleşir ve forma dahil edilmez. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.Disabled.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'visible',
|
path: 'visible',
|
||||||
label: 'visible',
|
label: 'visible',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
help: 'Editörü tamamen gizler. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.Visible.Help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'hint',
|
||||||
|
label: 'hint',
|
||||||
|
type: 'text',
|
||||||
|
group: 'common',
|
||||||
|
help: 'ListForms.EditorOptions.Spec.Hint.Help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tabIndex',
|
||||||
|
label: 'tabIndex',
|
||||||
|
type: 'number',
|
||||||
|
group: 'common',
|
||||||
|
help: 'ListForms.EditorOptions.Spec.TabIndex.Help',
|
||||||
},
|
},
|
||||||
{ path: 'hint', label: 'hint', type: 'text', group: 'common', help: 'Fare ile üzerine gelince çıkan ipucu.' },
|
|
||||||
{ path: 'tabIndex', label: 'tabIndex', type: 'number', group: 'common', help: 'Tab ile gezinme sırası.' },
|
|
||||||
{
|
{
|
||||||
path: 'valueChangeEvent',
|
path: 'valueChangeEvent',
|
||||||
label: 'valueChangeEvent',
|
label: 'valueChangeEvent',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
placeholder: 'change / input / keyup',
|
placeholder: 'change / input / keyup',
|
||||||
help: 'Değerin hangi DOM olayında işleneceği. Anlık kaydetme için input.',
|
help: 'ListForms.EditorOptions.Spec.ValueChangeEvent.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'validationMessageMode',
|
path: 'validationMessageMode',
|
||||||
|
|
@ -183,14 +220,14 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
placeholder: 'my-editor-class',
|
placeholder: 'my-editor-class',
|
||||||
help: 'Editör kök elemanına eklenecek CSS sınıfı.',
|
help: 'ListForms.EditorOptions.Spec.ElementAttrClass.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'inputAttr.aria-label',
|
path: 'inputAttr.aria-label',
|
||||||
label: 'inputAttr.aria-label',
|
label: 'inputAttr.aria-label',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
help: 'Erişilebilirlik etiketi.',
|
help: 'ListForms.EditorOptions.Spec.InputAttrAriaLabel.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'inputAttr.style',
|
path: 'inputAttr.style',
|
||||||
|
|
@ -198,7 +235,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'common',
|
group: 'common',
|
||||||
placeholder: 'text-align: right',
|
placeholder: 'text-align: right',
|
||||||
help: 'Girdi alanına doğrudan inline CSS uygular. Sayısal alanları sağa yaslamak için kullanışlıdır.',
|
help: 'ListForms.EditorOptions.Spec.InputAttrStyle.Help',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Görünüm ──────────────────────────────────────────────────────
|
// ── Görünüm ──────────────────────────────────────────────────────
|
||||||
|
|
@ -208,7 +245,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'size',
|
type: 'size',
|
||||||
group: 'appearance',
|
group: 'appearance',
|
||||||
placeholder: '100% veya 240',
|
placeholder: '100% veya 240',
|
||||||
help: 'Sadece rakam yazarsan sayı olarak kaydedilir (240). Yüzde için 100% yaz.',
|
help: 'ListForms.EditorOptions.Spec.Width.Help',
|
||||||
},
|
},
|
||||||
{ path: 'height', label: 'height', type: 'size', group: 'appearance', placeholder: '200' },
|
{ path: 'height', label: 'height', type: 'size', group: 'appearance', placeholder: '200' },
|
||||||
{
|
{
|
||||||
|
|
@ -216,9 +253,15 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
label: 'placeholder',
|
label: 'placeholder',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'appearance',
|
group: 'appearance',
|
||||||
help: 'Alan boşken görünen metin. Sütunun PlaceHolder alanını ezer.',
|
help: 'ListForms.EditorOptions.Spec.Placeholder.Help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'label',
|
||||||
|
label: 'label',
|
||||||
|
type: 'text',
|
||||||
|
group: 'appearance',
|
||||||
|
help: 'ListForms.EditorOptions.Spec.Label.Help',
|
||||||
},
|
},
|
||||||
{ path: 'label', label: 'label', type: 'text', group: 'appearance', help: 'Editör içi etiket metni.' },
|
|
||||||
{
|
{
|
||||||
path: 'labelMode',
|
path: 'labelMode',
|
||||||
label: 'labelMode',
|
label: 'labelMode',
|
||||||
|
|
@ -247,7 +290,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
label: 'showClearButton',
|
label: 'showClearButton',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'appearance',
|
group: 'appearance',
|
||||||
help: 'Temizleme (x) butonu. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.ShowClearButton.Help',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Metin / Maske ────────────────────────────────────────────────
|
// ── Metin / Maske ────────────────────────────────────────────────
|
||||||
|
|
@ -275,7 +318,14 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
editors: TEXT_EDITORS,
|
editors: TEXT_EDITORS,
|
||||||
placeholder: '(000) 000-0000',
|
placeholder: '(000) 000-0000',
|
||||||
},
|
},
|
||||||
{ path: 'maskChar', label: 'maskChar', type: 'text', group: 'text', editors: TEXT_EDITORS, placeholder: '_' },
|
{
|
||||||
|
path: 'maskChar',
|
||||||
|
label: 'maskChar',
|
||||||
|
type: 'text',
|
||||||
|
group: 'text',
|
||||||
|
editors: TEXT_EDITORS,
|
||||||
|
placeholder: '_',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'maskInvalidMessage',
|
path: 'maskInvalidMessage',
|
||||||
label: 'maskInvalidMessage',
|
label: 'maskInvalidMessage',
|
||||||
|
|
@ -300,7 +350,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'text',
|
group: 'text',
|
||||||
editors: TEXT_EDITORS,
|
editors: TEXT_EDITORS,
|
||||||
help: 'true ise maske karakterleri de veritabanına yazılır. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.UseMaskedValue.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'maskRules.X',
|
path: 'maskRules.X',
|
||||||
|
|
@ -309,16 +359,22 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'text',
|
group: 'text',
|
||||||
editors: TEXT_EDITORS,
|
editors: TEXT_EDITORS,
|
||||||
placeholder: '[0-9]',
|
placeholder: '[0-9]',
|
||||||
help: 'Maskede X karakterinin karşılığı olan düzenli ifade.',
|
help: 'ListForms.EditorOptions.Spec.MaskRulesX.Help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'spellcheck',
|
||||||
|
label: 'spellcheck',
|
||||||
|
type: 'boolean',
|
||||||
|
group: 'text',
|
||||||
|
editors: TEXT_EDITORS,
|
||||||
},
|
},
|
||||||
{ path: 'spellcheck', label: 'spellcheck', type: 'boolean', group: 'text', editors: TEXT_EDITORS },
|
|
||||||
{
|
{
|
||||||
path: 'autoResizeEnabled',
|
path: 'autoResizeEnabled',
|
||||||
label: 'autoResizeEnabled',
|
label: 'autoResizeEnabled',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'text',
|
group: 'text',
|
||||||
editors: ['dxTextArea'],
|
editors: ['dxTextArea'],
|
||||||
help: 'İçerik büyüdükçe alan uzar. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.AutoResizeEnabled.Help',
|
||||||
},
|
},
|
||||||
{ path: 'minHeight', label: 'minHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
|
{ path: 'minHeight', label: 'minHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
|
||||||
{ path: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
|
{ path: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
|
||||||
|
|
@ -347,7 +403,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'select',
|
type: 'select',
|
||||||
group: 'number',
|
group: 'number',
|
||||||
editors: ['dxNumberBox'],
|
editors: ['dxNumberBox'],
|
||||||
help: 'Sayısal format tipi. Seçilirse format alanı nesne olur.',
|
help: 'ListForms.EditorOptions.Spec.FormatType.Help',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: 'fixedPoint', label: 'fixedPoint' },
|
{ value: 'fixedPoint', label: 'fixedPoint' },
|
||||||
{ value: 'decimal', label: 'decimal' },
|
{ value: 'decimal', label: 'decimal' },
|
||||||
|
|
@ -366,7 +422,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'number',
|
type: 'number',
|
||||||
group: 'number',
|
group: 'number',
|
||||||
editors: ['dxNumberBox'],
|
editors: ['dxNumberBox'],
|
||||||
help: 'Ondalık basamak sayısı.',
|
help: 'ListForms.EditorOptions.Spec.FormatPrecision.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'format.currency',
|
path: 'format.currency',
|
||||||
|
|
@ -389,7 +445,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'number',
|
group: 'number',
|
||||||
editors: ['dxNumberBox', 'dxDateBox'],
|
editors: ['dxNumberBox', 'dxDateBox'],
|
||||||
help: 'Girdiyi formatlı maske olarak yönetir. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.UseMaskBehavior.Help',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Tarih ────────────────────────────────────────────────────────
|
// ── Tarih ────────────────────────────────────────────────────────
|
||||||
|
|
@ -425,7 +481,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'date',
|
group: 'date',
|
||||||
editors: DATE_EDITORS,
|
editors: DATE_EDITORS,
|
||||||
placeholder: 'dd/MM/yyyy',
|
placeholder: 'dd/MM/yyyy',
|
||||||
help: 'Ekranda gösterilecek biçim. Listedeki sütun biçimini de ezer.',
|
help: 'ListForms.EditorOptions.Spec.DisplayFormat.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dateSerializationFormat',
|
path: 'dateSerializationFormat',
|
||||||
|
|
@ -434,7 +490,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'date',
|
group: 'date',
|
||||||
editors: DATE_EDITORS,
|
editors: DATE_EDITORS,
|
||||||
placeholder: 'yyyy-MM-ddTHH:mm:ss',
|
placeholder: 'yyyy-MM-ddTHH:mm:ss',
|
||||||
help: 'Veritabanına yazılacak biçim. Tarih kaymalarının çoğu bu alandan kaynaklanır.',
|
help: 'ListForms.EditorOptions.Spec.DateSerializationFormat.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'interval',
|
path: 'interval',
|
||||||
|
|
@ -442,11 +498,11 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'number',
|
type: 'number',
|
||||||
group: 'date',
|
group: 'date',
|
||||||
editors: DATE_EDITORS,
|
editors: DATE_EDITORS,
|
||||||
help: 'Saat listesinde dakika aralığı.',
|
help: 'ListForms.EditorOptions.Spec.Interval.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'min',
|
path: 'min',
|
||||||
label: 'min (tarih)',
|
label: 'min',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'date',
|
group: 'date',
|
||||||
editors: DATE_EDITORS,
|
editors: DATE_EDITORS,
|
||||||
|
|
@ -454,7 +510,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'max',
|
path: 'max',
|
||||||
label: 'max (tarih)',
|
label: 'max',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'date',
|
group: 'date',
|
||||||
editors: DATE_EDITORS,
|
editors: DATE_EDITORS,
|
||||||
|
|
@ -515,7 +571,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'dropdown',
|
group: 'dropdown',
|
||||||
editors: DROPDOWN_EDITORS,
|
editors: DROPDOWN_EDITORS,
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Listede arama kutusu. TagBox için backend TagBoxOptions olarak da okur. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.SearchEnabled.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'searchMode',
|
path: 'searchMode',
|
||||||
|
|
@ -535,7 +591,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'dropdown',
|
group: 'dropdown',
|
||||||
editors: DROPDOWN_EDITORS,
|
editors: DROPDOWN_EDITORS,
|
||||||
placeholder: 'name',
|
placeholder: 'name',
|
||||||
help: 'Aramanın hangi alanda yapılacağı.',
|
help: 'ListForms.EditorOptions.Spec.SearchExpr.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'searchTimeout',
|
path: 'searchTimeout',
|
||||||
|
|
@ -565,7 +621,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'dropdown',
|
group: 'dropdown',
|
||||||
editors: DROPDOWN_EDITORS,
|
editors: DROPDOWN_EDITORS,
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Listede olmayan değerin yazılmasına izin verir. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.AcceptCustomValue.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'noDataText',
|
path: 'noDataText',
|
||||||
|
|
@ -573,7 +629,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'dropdown',
|
group: 'dropdown',
|
||||||
editors: DROPDOWN_EDITORS,
|
editors: DROPDOWN_EDITORS,
|
||||||
placeholder: 'Kayıt bulunamadı',
|
placeholder: 'ListForms.EditorOptions.Spec.NoDataText.Placeholder',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'deferRendering',
|
path: 'deferRendering',
|
||||||
|
|
@ -581,7 +637,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'dropdown',
|
group: 'dropdown',
|
||||||
editors: DROPDOWN_EDITORS,
|
editors: DROPDOWN_EDITORS,
|
||||||
help: 'Liste yalnızca açıldığında render edilir. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.DeferRendering.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wrapItemText',
|
path: 'wrapItemText',
|
||||||
|
|
@ -620,7 +676,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'tagBox',
|
group: 'tagBox',
|
||||||
editors: ['dxTagBox'],
|
editors: ['dxTagBox'],
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Liste öğelerinde onay kutusu gösterir. Boş bırakılırsa platform varsayılanı true. ',
|
help: 'ListForms.EditorOptions.Spec.ShowSelectionControls.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'maxDisplayedTags',
|
path: 'maxDisplayedTags',
|
||||||
|
|
@ -645,13 +701,19 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'tagBox',
|
group: 'tagBox',
|
||||||
editors: ['dxTagBox'],
|
editors: ['dxTagBox'],
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Boş bırakılırsa platform varsayılanı useButtons.',
|
help: 'ListForms.EditorOptions.Spec.ApplyValueMode.Help',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: 'instantly', label: 'instantly' },
|
{ value: 'instantly', label: 'instantly' },
|
||||||
{ value: 'useButtons', label: 'useButtons' },
|
{ value: 'useButtons', label: 'useButtons' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: 'multiline', label: 'multiline', type: 'boolean', group: 'tagBox', editors: ['dxTagBox'] },
|
{
|
||||||
|
path: 'multiline',
|
||||||
|
label: 'multiline',
|
||||||
|
type: 'boolean',
|
||||||
|
group: 'tagBox',
|
||||||
|
editors: ['dxTagBox'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'hideSelectedItems',
|
path: 'hideSelectedItems',
|
||||||
label: 'hideSelectedItems',
|
label: 'hideSelectedItems',
|
||||||
|
|
@ -669,7 +731,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
editors: ['dxGridBox'],
|
editors: ['dxGridBox'],
|
||||||
platform: true,
|
platform: true,
|
||||||
placeholder: 'key, name, group',
|
placeholder: 'key, name, group',
|
||||||
help: 'Açılır tabloda gösterilecek sütunlar. Virgülle ayır; JSON\'a dizi olarak yazılır.',
|
help: 'ListForms.EditorOptions.Spec.Columns.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'selectionMode',
|
path: 'selectionMode',
|
||||||
|
|
@ -678,7 +740,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'gridBox',
|
group: 'gridBox',
|
||||||
editors: ['dxGridBox'],
|
editors: ['dxGridBox'],
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'multiple seçilirse liste sütunu da çoklu değer olarak çalışır.',
|
help: 'ListForms.EditorOptions.Spec.SelectionMode.Help',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: 'single', label: 'single' },
|
{ value: 'single', label: 'single' },
|
||||||
{ value: 'multiple', label: 'multiple' },
|
{ value: 'multiple', label: 'multiple' },
|
||||||
|
|
@ -692,7 +754,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'gridBox',
|
group: 'gridBox',
|
||||||
editors: ['dxGridBox'],
|
editors: ['dxGridBox'],
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Açılır tabloda filtre satırı. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.FilterRowVisible.Help',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Görsel ───────────────────────────────────────────────────────
|
// ── Görsel ───────────────────────────────────────────────────────
|
||||||
|
|
@ -703,7 +765,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'image',
|
group: 'image',
|
||||||
editors: IMAGE_EDITORS,
|
editors: IMAGE_EDITORS,
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Boş bırakılırsa görsel base64 olarak saklanır.',
|
help: 'ListForms.EditorOptions.Spec.UploadUrl.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'fileFieldName',
|
path: 'fileFieldName',
|
||||||
|
|
@ -712,7 +774,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'image',
|
group: 'image',
|
||||||
editors: IMAGE_EDITORS,
|
editors: IMAGE_EDITORS,
|
||||||
placeholder: 'file',
|
placeholder: 'file',
|
||||||
help: 'Yükleme isteğindeki form alan adı.',
|
help: 'ListForms.EditorOptions.Spec.FileFieldName.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'accept',
|
path: 'accept',
|
||||||
|
|
@ -730,7 +792,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
group: 'image',
|
group: 'image',
|
||||||
editors: IMAGE_EDITORS,
|
editors: IMAGE_EDITORS,
|
||||||
platform: true,
|
platform: true,
|
||||||
help: 'Çoklu görsel. Metin olarak "true" yazılırsa da çalışır ama boolean tercih edilmeli.',
|
help: 'ListForms.EditorOptions.Spec.Multiple.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'maxFileSize',
|
path: 'maxFileSize',
|
||||||
|
|
@ -748,10 +810,22 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'choice',
|
group: 'choice',
|
||||||
editors: ['dxCheckBox'],
|
editors: ['dxCheckBox'],
|
||||||
help: 'Onay kutusunun yanında görünen metin.',
|
help: 'ListForms.EditorOptions.Spec.Text.Help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'switchedOnText',
|
||||||
|
label: 'switchedOnText',
|
||||||
|
type: 'text',
|
||||||
|
group: 'choice',
|
||||||
|
editors: ['dxSwitch'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'switchedOffText',
|
||||||
|
label: 'switchedOffText',
|
||||||
|
type: 'text',
|
||||||
|
group: 'choice',
|
||||||
|
editors: ['dxSwitch'],
|
||||||
},
|
},
|
||||||
{ path: 'switchedOnText', label: 'switchedOnText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
|
|
||||||
{ path: 'switchedOffText', label: 'switchedOffText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
|
|
||||||
{
|
{
|
||||||
path: 'layout',
|
path: 'layout',
|
||||||
label: 'layout',
|
label: 'layout',
|
||||||
|
|
@ -794,7 +868,13 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
{ value: 'always', label: 'always' },
|
{ value: 'always', label: 'always' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: 'showRange', label: 'showRange', type: 'boolean', group: 'slider', editors: SLIDER_EDITORS },
|
{
|
||||||
|
path: 'showRange',
|
||||||
|
label: 'showRange',
|
||||||
|
type: 'boolean',
|
||||||
|
group: 'slider',
|
||||||
|
editors: SLIDER_EDITORS,
|
||||||
|
},
|
||||||
|
|
||||||
// ── HTML editör ──────────────────────────────────────────────────
|
// ── HTML editör ──────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
|
|
@ -821,7 +901,7 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'json',
|
type: 'json',
|
||||||
group: 'html',
|
group: 'html',
|
||||||
editors: ['dxHtmlEditor'],
|
editors: ['dxHtmlEditor'],
|
||||||
help: 'Araç çubuğu tanımı. Hazır ayarlardaki "htmlEditor toolbar" bunu doldurur.',
|
help: 'ListForms.EditorOptions.Spec.ToolbarItems.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mediaResizing.enabled',
|
path: 'mediaResizing.enabled',
|
||||||
|
|
@ -857,21 +937,21 @@ export const optionSpecs: OptionSpec[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
group: 'grid',
|
group: 'grid',
|
||||||
placeholder: 'dd/MM/yyyy veya currency',
|
placeholder: 'dd/MM/yyyy veya currency',
|
||||||
help: 'Metin olarak yazılırsa listedeki sütun biçimini de ezer. Sayı formatı için Sayı bölümündeki format.type alanını kullan.',
|
help: 'ListForms.EditorOptions.Spec.Format.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'encodeHtml',
|
path: 'encodeHtml',
|
||||||
label: 'encodeHtml',
|
label: 'encodeHtml',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
group: 'grid',
|
group: 'grid',
|
||||||
help: 'false yapılırsa hücre içeriği HTML olarak render edilir. ' + booleanHelp,
|
help: 'ListForms.EditorOptions.Spec.EncodeHtml.Help',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'buttons',
|
path: 'buttons',
|
||||||
label: 'buttons',
|
label: 'buttons',
|
||||||
type: 'json',
|
type: 'json',
|
||||||
group: 'grid',
|
group: 'grid',
|
||||||
help: 'Editör içi butonlar. options.onClick metin olarak yazılırsa çalışma anında fonksiyona çevrilir.',
|
help: 'ListForms.EditorOptions.Spec.Buttons.Help',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,32 +96,32 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
{
|
{
|
||||||
key: 'readOnly',
|
key: 'readOnly',
|
||||||
label: 'Salt okunur',
|
label: 'Salt okunur',
|
||||||
description: 'readOnly: true — alan görünür ama değiştirilemez.',
|
description: 'ListForms.EditorOptions.Preset.ReadOnly.Description',
|
||||||
value: { readOnly: true },
|
value: { readOnly: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'disabled',
|
key: 'disabled',
|
||||||
label: 'Pasif',
|
label: 'Pasif',
|
||||||
description: 'disabled: true — alan tamamen pasifleşir.',
|
description: 'ListForms.EditorOptions.Preset.Disabled.Description',
|
||||||
value: { disabled: true },
|
value: { disabled: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rightAligned',
|
key: 'rightAligned',
|
||||||
label: 'Sağa yaslı',
|
label: 'ListForms.EditorOptions.Preset.RightAligned.Label',
|
||||||
description: 'Girdi metnini sağa yaslar. Tutar/miktar alanları için.',
|
description: 'ListForms.EditorOptions.Preset.RightAligned.Description',
|
||||||
value: { inputAttr: { style: 'text-align: right' } },
|
value: { inputAttr: { style: 'text-align: right' } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'fixedPoint2',
|
key: 'fixedPoint2',
|
||||||
label: 'Ondalık (2 hane)',
|
label: 'ListForms.EditorOptions.Preset.FixedPoint2.Label',
|
||||||
description: 'format.type fixedPoint, precision 2.',
|
description: 'format.type fixedPoint, precision 2.',
|
||||||
editors: ['dxNumberBox'],
|
editors: ['dxNumberBox'],
|
||||||
value: { format: { type: 'fixedPoint', precision: 2 } },
|
value: { format: { type: 'fixedPoint', precision: 2 } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'numberSpin2',
|
key: 'numberSpin2',
|
||||||
label: 'Ondalık + spin',
|
label: 'ListForms.EditorOptions.Preset.NumberSpin2.Label',
|
||||||
description: 'precision 2, mask davranışı ve artır/azalt butonları.',
|
description: 'ListForms.EditorOptions.Preset.NumberSpin2.Description',
|
||||||
editors: ['dxNumberBox'],
|
editors: ['dxNumberBox'],
|
||||||
value: {
|
value: {
|
||||||
format: { type: 'fixedPoint', precision: 2 },
|
format: { type: 'fixedPoint', precision: 2 },
|
||||||
|
|
@ -132,14 +132,14 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
{
|
{
|
||||||
key: 'currency',
|
key: 'currency',
|
||||||
label: 'Para birimi (TRY)',
|
label: 'Para birimi (TRY)',
|
||||||
description: 'currency formatı, 2 ondalık.',
|
description: 'ListForms.EditorOptions.Preset.Currency.Description',
|
||||||
editors: ['dxNumberBox'],
|
editors: ['dxNumberBox'],
|
||||||
value: { format: { type: 'currency', precision: 2, currency: 'TRY' } },
|
value: { format: { type: 'currency', precision: 2, currency: 'TRY' } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'date',
|
key: 'date',
|
||||||
label: 'Tarih dd/MM/yyyy',
|
label: 'ListForms.EditorOptions.Preset.DateFormat.Label',
|
||||||
description: 'Görünüm ve veritabanı biçimini birlikte ayarlar.',
|
description: 'ListForms.EditorOptions.Preset.Date.Description',
|
||||||
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
|
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
|
||||||
value: {
|
value: {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
|
|
@ -149,8 +149,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dateTime',
|
key: 'dateTime',
|
||||||
label: 'Tarih + saat',
|
label: 'ListForms.EditorOptions.Preset.DateTime.Label',
|
||||||
description: 'dd/MM/yyyy HH:mm görünümü, ISO serileştirme.',
|
description: 'ListForms.EditorOptions.Preset.DateTime.Description',
|
||||||
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
|
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
|
||||||
value: {
|
value: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
|
|
@ -160,8 +160,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'timePicker',
|
key: 'timePicker',
|
||||||
label: 'Saat seçici',
|
label: 'ListForms.EditorOptions.Preset.TimePicker.Label',
|
||||||
description: '5 dakika aralıklı liste tipi saat seçimi.',
|
description: 'ListForms.EditorOptions.Preset.TimePicker.Description',
|
||||||
editors: ['dxDateBox'],
|
editors: ['dxDateBox'],
|
||||||
value: {
|
value: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
|
|
@ -175,7 +175,7 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
{
|
{
|
||||||
key: 'phoneMask',
|
key: 'phoneMask',
|
||||||
label: 'Telefon maskesi',
|
label: 'Telefon maskesi',
|
||||||
description: 'Maske, yer tutucu ve hata mesajı.',
|
description: 'ListForms.EditorOptions.Preset.PhoneMask.Description',
|
||||||
editors: ['dxTextBox', 'dxAutocomplete'],
|
editors: ['dxTextBox', 'dxAutocomplete'],
|
||||||
value: {
|
value: {
|
||||||
mask: '(000) 000-0000',
|
mask: '(000) 000-0000',
|
||||||
|
|
@ -187,22 +187,34 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'textAreaAuto',
|
key: 'textAreaAuto',
|
||||||
label: 'Otomatik büyüyen alan',
|
label: 'ListForms.EditorOptions.Preset.TextAreaAuto.Label',
|
||||||
description: 'autoResizeEnabled ve minimum yükseklik.',
|
description: 'ListForms.EditorOptions.Preset.TextAreaAuto.Description',
|
||||||
editors: ['dxTextArea'],
|
editors: ['dxTextArea'],
|
||||||
value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 },
|
value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'searchableList',
|
key: 'searchableList',
|
||||||
label: 'Aranabilir liste',
|
label: 'Aranabilir liste',
|
||||||
description: 'İçinde geçen kayıtları arar, 300 ms gecikme.',
|
description: 'ListForms.EditorOptions.Preset.SearchableList.Description',
|
||||||
editors: ['dxSelectBox', 'dxLookup', 'dxTagBox', 'dxDropDownBox', 'dxGridBox', 'dxAutocomplete'],
|
editors: [
|
||||||
value: { searchEnabled: true, searchMode: 'contains', searchTimeout: 300, showClearButton: true },
|
'dxSelectBox',
|
||||||
|
'dxLookup',
|
||||||
|
'dxTagBox',
|
||||||
|
'dxDropDownBox',
|
||||||
|
'dxGridBox',
|
||||||
|
'dxAutocomplete',
|
||||||
|
],
|
||||||
|
value: {
|
||||||
|
searchEnabled: true,
|
||||||
|
searchMode: 'contains',
|
||||||
|
searchTimeout: 300,
|
||||||
|
showClearButton: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tagBoxInstant',
|
key: 'tagBoxInstant',
|
||||||
label: 'TagBox anlık seçim',
|
label: 'ListForms.EditorOptions.Preset.TagBoxInstant.Label',
|
||||||
description: 'Onay kutulu, butonsuz, anında uygulanan çoklu seçim.',
|
description: 'ListForms.EditorOptions.Preset.TagBoxInstant.Description',
|
||||||
editors: ['dxTagBox'],
|
editors: ['dxTagBox'],
|
||||||
value: {
|
value: {
|
||||||
showSelectionControls: true,
|
showSelectionControls: true,
|
||||||
|
|
@ -213,8 +225,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'gridBoxSingle',
|
key: 'gridBoxSingle',
|
||||||
label: 'GridBox tekli seçim',
|
label: 'ListForms.EditorOptions.Preset.GridBoxSingle.Label',
|
||||||
description: 'key/name sütunları, filtre satırı açık, tek seçim.',
|
description: 'ListForms.EditorOptions.Preset.GridBoxSingle.Description',
|
||||||
editors: ['dxGridBox'],
|
editors: ['dxGridBox'],
|
||||||
value: {
|
value: {
|
||||||
columns: ['key', 'name'],
|
columns: ['key', 'name'],
|
||||||
|
|
@ -227,8 +239,8 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'gridBoxMulti',
|
key: 'gridBoxMulti',
|
||||||
label: 'GridBox çoklu seçim',
|
label: 'ListForms.EditorOptions.Preset.GridBoxMulti.Label',
|
||||||
description: 'Çoklu seçim; liste sütunu da çoklu değer olarak çalışır.',
|
description: 'ListForms.EditorOptions.Preset.GridBoxMulti.Description',
|
||||||
editors: ['dxGridBox'],
|
editors: ['dxGridBox'],
|
||||||
value: {
|
value: {
|
||||||
columns: ['key', 'name'],
|
columns: ['key', 'name'],
|
||||||
|
|
@ -240,27 +252,27 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'imageMulti',
|
key: 'imageMulti',
|
||||||
label: 'Çoklu görsel 80x80',
|
label: 'ListForms.EditorOptions.Preset.ImageMulti.Label',
|
||||||
description: 'Çoklu yükleme ve 80x80 küçük resim.',
|
description: 'ListForms.EditorOptions.Preset.ImageMulti.Description',
|
||||||
editors: ['dxImageUpload', 'dxImageViewer'],
|
editors: ['dxImageUpload', 'dxImageViewer'],
|
||||||
value: { width: 80, height: 80, multiple: true, accept: 'image/*' },
|
value: { width: 80, height: 80, multiple: true, accept: 'image/*' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'htmlToolbar',
|
key: 'htmlToolbar',
|
||||||
label: 'HtmlEditor araç çubuğu',
|
label: 'ListForms.EditorOptions.Preset.HtmlToolbar.Label',
|
||||||
description: 'Tam araç çubuğu, görsel yükleme ve boyutlandırma.',
|
description: 'ListForms.EditorOptions.Preset.HtmlToolbar.Description',
|
||||||
editors: ['dxHtmlEditor'],
|
editors: ['dxHtmlEditor'],
|
||||||
value: buildHtmlEditorOptions(),
|
value: buildHtmlEditorOptions(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'height100',
|
key: 'height100',
|
||||||
label: 'Yükseklik 100',
|
label: 'ListForms.EditorOptions.Preset.Height100.Label',
|
||||||
description: 'height: 100',
|
description: 'height: 100',
|
||||||
value: { height: 100 },
|
value: { height: 100 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'height200',
|
key: 'height200',
|
||||||
label: 'Yükseklik 200',
|
label: 'ListForms.EditorOptions.Preset.Height200.Label',
|
||||||
description: 'height: 200',
|
description: 'height: 200',
|
||||||
value: { height: 200 },
|
value: { height: 200 },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
type RecipeId,
|
type RecipeId,
|
||||||
type RecipeParam,
|
type RecipeParam,
|
||||||
type ScriptRule as FormScriptRule,
|
type ScriptRule as FormScriptRule,
|
||||||
|
type TranslateFn,
|
||||||
} from './scriptRecipes'
|
} from './scriptRecipes'
|
||||||
|
|
||||||
const groupIcons: Record<RecipeGroupKey, ScriptRecipeGroup['icon']> = {
|
const groupIcons: Record<RecipeGroupKey, ScriptRecipeGroup['icon']> = {
|
||||||
|
|
@ -52,8 +53,7 @@ const FIELD_SOURCE = 'field'
|
||||||
const toParam = (param: RecipeParam): ScriptRecipeParam => ({
|
const toParam = (param: RecipeParam): ScriptRecipeParam => ({
|
||||||
key: param.key,
|
key: param.key,
|
||||||
label: param.label,
|
label: param.label,
|
||||||
type:
|
type: param.type === 'field' ? 'option' : param.type === 'fieldList' ? 'fieldList' : param.type,
|
||||||
param.type === 'field' ? 'option' : param.type === 'fieldList' ? 'fieldList' : param.type,
|
|
||||||
optionSource: param.type === 'field' || param.type === 'fieldList' ? FIELD_SOURCE : undefined,
|
optionSource: param.type === 'field' || param.type === 'fieldList' ? FIELD_SOURCE : undefined,
|
||||||
placeholder: param.placeholder,
|
placeholder: param.placeholder,
|
||||||
help: param.help,
|
help: param.help,
|
||||||
|
|
@ -79,57 +79,57 @@ const apiReference: ScriptApiEntry[] = runtimeApiReference
|
||||||
*/
|
*/
|
||||||
const snippets: ScriptSnippet[] = [
|
const snippets: ScriptSnippet[] = [
|
||||||
{
|
{
|
||||||
label: 'Koşullu değer yaz',
|
label: 'ListForms.ScriptBuilder.Snippet.ConditionalSet.Label',
|
||||||
group: 'Akış',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
|
||||||
description: 'Bir alan doluysa hedefe yaz.',
|
description: 'ListForms.ScriptBuilder.Snippet.ConditionalSet.Description',
|
||||||
code: "if (get('${1:Kaynak}')) set('${2:Hedef}', get('${1:Kaynak}'))",
|
code: "if (get('${1:Kaynak}')) set('${2:Hedef}', get('${1:Kaynak}'))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Çok koşullu blok',
|
label: 'ListForms.ScriptBuilder.Snippet.MultiCondition.Label',
|
||||||
group: 'Akış',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
|
||||||
description: 'VE / VEYA ile birleşen iki koşul.',
|
description: 'ListForms.ScriptBuilder.Snippet.MultiCondition.Description',
|
||||||
code: "if (str('${1:Durum}') === '${2:Aktif}' && num('${3:Tutar}') > ${4:1000}) {\n ${5:notify('Limit aşıldı')}\n}",
|
code: "if (str('${1:Durum}') === '${2:Aktif}' && num('${3:Tutar}') > ${4:1000}) {\n ${5:notify('Limit aşıldı')}\n}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Açılışta / değişimde ayrımı',
|
label: 'ListForms.ScriptBuilder.Snippet.OpenVsChange.Label',
|
||||||
group: 'Akış',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Flow',
|
||||||
description: 'isReady form açılışında true gelir.',
|
description: 'ListForms.ScriptBuilder.Snippet.OpenVsChange.Description',
|
||||||
code: "if (isReady) {\n ${1:// form açılırken}\n} else {\n ${2:// değer değişince}\n}",
|
code: 'if (isReady) {\n ${1:// form açılırken}\n} else {\n ${2:// değer değişince}\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Toplu alan yazma',
|
label: 'ListForms.ScriptBuilder.Snippet.BulkSet.Label',
|
||||||
group: 'Veri',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Data',
|
||||||
description: 'Tek çağrıda birden fazla alan.',
|
description: 'ListForms.ScriptBuilder.Snippet.BulkSet.Description',
|
||||||
code: "set({ ${1:Alan1}: ${2:null}, ${3:Alan2}: ${4:null} })",
|
code: 'set({ ${1:Alan1}: ${2:null}, ${3:Alan2}: ${4:null} })',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Seçili kayıttan doldur',
|
label: 'ListForms.ScriptBuilder.Snippet.FillFromSelected.Label',
|
||||||
group: 'Veri',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Data',
|
||||||
description: 'Lookup seçiminden birkaç alanı birden taşı.',
|
description: 'ListForms.ScriptBuilder.Snippet.FillFromSelected.Description',
|
||||||
code: "copy('${1:kolon}', '${2:Hedef}')\ncopy('${3:kolon2}', '${4:Hedef2}')",
|
code: "copy('${1:kolon}', '${2:Hedef}')\ncopy('${3:kolon2}', '${4:Hedef2}')",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Onay iste, vazgeçilirse geri al',
|
label: 'ListForms.ScriptBuilder.Snippet.AskConfirm.Label',
|
||||||
group: 'Etkileşim',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Interaction',
|
||||||
description: 'ask false dönerse alan eski değerine döner.',
|
description: 'ListForms.ScriptBuilder.Snippet.AskConfirm.Description',
|
||||||
code: "if (!ask('${1:Emin misiniz?}')) return",
|
code: "if (!ask('${1:Emin misiniz?}')) return",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'API çağrısı ve hata yakalama',
|
label: 'ListForms.ScriptBuilder.Snippet.ApiCall.Label',
|
||||||
group: 'Entegrasyon',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Integration',
|
||||||
description: 'Çağrı başarısızsa uyarı gösterir.',
|
description: 'ListForms.ScriptBuilder.Snippet.ApiCall.Description',
|
||||||
code: "try {\n set('${1:Hedef}', await api('${2:/api/app/musteri/{value}}', '${3:data.name}'))\n} catch (error) {\n notify('${4:Servise ulaşılamadı}')\n}",
|
code: "try {\n set('${1:Hedef}', await api('${2:/api/app/musteri/{value}}', '${3:data.name}'))\n} catch (error) {\n notify('${4:Servise ulaşılamadı}')\n}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hesaplama ve yuvarlama',
|
label: 'ListForms.ScriptBuilder.Snippet.Calc.Label',
|
||||||
group: 'Hesaplama',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.Calc',
|
||||||
description: 'İki alandan türetilmiş değer.',
|
description: 'ListForms.ScriptBuilder.Snippet.Calc.Description',
|
||||||
code: "set('${1:Toplam}', round(num('${2:Miktar}') * num('${3:Fiyat}'), ${4:2}))",
|
code: "set('${1:Toplam}', round(num('${2:Miktar}') * num('${3:Fiyat}'), ${4:2}))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Alanı koşula göre kilitle',
|
label: 'ListForms.ScriptBuilder.Snippet.LockField.Label',
|
||||||
group: 'Görünüm',
|
group: 'ListForms.ScriptBuilder.Snippet.Group.View',
|
||||||
description: 'Koşul doğruyken salt okunur olur.',
|
description: 'ListForms.ScriptBuilder.Snippet.LockField.Description',
|
||||||
code: "readOnly('${1:Alan}', str('${2:Durum}') === '${3:Kapali}')",
|
code: "readOnly('${1:Alan}', str('${2:Durum}') === '${3:Kapali}')",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -139,9 +139,12 @@ export type FormScriptDialectContext = {
|
||||||
fields: string[]
|
fields: string[]
|
||||||
/** Script'in bağlı olduğu alan; başlıkta gösterilir. */
|
/** Script'in bağlı olduğu alan; başlıkta gösterilir. */
|
||||||
currentField?: string
|
currentField?: string
|
||||||
|
/** `useLocalization` hook'undan gelen çeviri fonksiyonu. */
|
||||||
|
translate: TranslateFn
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createFormScriptDialect = (context: FormScriptDialectContext): ScriptDialect => {
|
export const createFormScriptDialect = (context: FormScriptDialectContext): ScriptDialect => {
|
||||||
|
const t = context.translate
|
||||||
const fields = Array.from(new Set(context.fields.filter(Boolean))).sort((left, right) =>
|
const fields = Array.from(new Set(context.fields.filter(Boolean))).sort((left, right) =>
|
||||||
left.localeCompare(right),
|
left.localeCompare(right),
|
||||||
)
|
)
|
||||||
|
|
@ -151,22 +154,51 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
|
||||||
return {
|
return {
|
||||||
id: 'form',
|
id: 'form',
|
||||||
title: 'Editor Script',
|
title: 'Editor Script',
|
||||||
subject: context.currentField || 'alan seçilmemiş',
|
subject: context.currentField || t('::ListForms.ScriptBuilder.NoFieldSelected'),
|
||||||
subjectHint: 'Bu alan değiştiğinde çalışacak script.',
|
subjectHint: t('::ListForms.ScriptBuilder.SubjectHint'),
|
||||||
codeHint:
|
codeHint: t('::ListForms.ScriptBuilder.CodeHint'),
|
||||||
'Tek satırlık yardımcılarla yaz; get/set/notify hazır. Ctrl+Space tamamlama listesini açar.',
|
|
||||||
groups: recipeGroups.map((group) => ({
|
groups: recipeGroups.map((group) => ({
|
||||||
key: group.key,
|
key: group.key,
|
||||||
title: group.title,
|
title: t('::' + group.title),
|
||||||
description: group.description,
|
description: t('::' + group.description),
|
||||||
icon: groupIcons[group.key],
|
icon: groupIcons[group.key],
|
||||||
})),
|
})),
|
||||||
recipes: recipeViews,
|
recipes: recipeViews.map((recipe) => ({
|
||||||
|
...recipe,
|
||||||
|
label: t('::' + recipe.label),
|
||||||
|
summary: t('::' + recipe.summary),
|
||||||
|
params: recipe.params.map((param) => ({
|
||||||
|
...param,
|
||||||
|
label: t('::' + param.label),
|
||||||
|
help: param.help ? t('::' + param.help) : undefined,
|
||||||
|
placeholder: param.placeholder?.startsWith('ListForms.')
|
||||||
|
? t('::' + param.placeholder)
|
||||||
|
: param.placeholder,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
operators: DEFAULT_SCRIPT_OPERATORS,
|
operators: DEFAULT_SCRIPT_OPERATORS,
|
||||||
conditionKinds: [{ value: FIELD_SOURCE, label: 'Form alanı', optionSource: FIELD_SOURCE }],
|
conditionKinds: [
|
||||||
triggers: triggerLabels,
|
{
|
||||||
apiReference,
|
value: FIELD_SOURCE,
|
||||||
snippets,
|
label: t('::ListForms.ScriptBuilder.FormField'),
|
||||||
|
optionSource: FIELD_SOURCE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
triggers: triggerLabels.map((trigger) => ({
|
||||||
|
...trigger,
|
||||||
|
label: t('::' + trigger.label),
|
||||||
|
help: t('::' + trigger.help),
|
||||||
|
})),
|
||||||
|
apiReference: apiReference.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
description: t('::' + entry.description),
|
||||||
|
})),
|
||||||
|
snippets: snippets.map((snippet) => ({
|
||||||
|
...snippet,
|
||||||
|
label: t('::' + snippet.label),
|
||||||
|
group: t('::' + snippet.group),
|
||||||
|
description: t('::' + snippet.description),
|
||||||
|
})),
|
||||||
getOptions: (optionSource) => (optionSource === FIELD_SOURCE ? fields : []),
|
getOptions: (optionSource) => (optionSource === FIELD_SOURCE ? fields : []),
|
||||||
getRecipe: (recipeId) => recipeViews.find((recipe) => recipe.id === recipeId),
|
getRecipe: (recipeId) => recipeViews.find((recipe) => recipe.id === recipeId),
|
||||||
createRule: (recipeId) => createRule(recipeId as RecipeId) as unknown as ScriptRule,
|
createRule: (recipeId) => createRule(recipeId as RecipeId) as unknown as ScriptRule,
|
||||||
|
|
@ -184,18 +216,23 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
|
||||||
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
|
return { ...parsed, rules: parsed.rules as unknown as ScriptRule[] }
|
||||||
},
|
},
|
||||||
describeCondition: (condition) =>
|
describeCondition: (condition) =>
|
||||||
describeCondition(condition as unknown as FormScriptRule['condition']),
|
describeCondition(condition as unknown as FormScriptRule['condition'], t),
|
||||||
describeRule: (rule) => {
|
describeRule: (rule) => {
|
||||||
const recipe = recipesById.get(rule.recipe as RecipeId)
|
const recipe = recipesById.get(rule.recipe as RecipeId)
|
||||||
if (!recipe) return ''
|
if (!recipe) return ''
|
||||||
const action = recipe.describe(asFormRule(rule))
|
const action = recipe.describe(asFormRule(rule), t)
|
||||||
if (recipe.conditionIsArgument) return action
|
if (recipe.conditionIsArgument) return action
|
||||||
|
const always = t('::ListForms.ScriptBuilder.Describe.Always')
|
||||||
const conditions = [rule.condition, ...(rule.conditions ?? [])]
|
const conditions = [rule.condition, ...(rule.conditions ?? [])]
|
||||||
.map((condition) => describeCondition(condition as unknown as FormScriptRule['condition']))
|
.map((condition) =>
|
||||||
.filter((text) => text !== 'Her zaman')
|
describeCondition(condition as unknown as FormScriptRule['condition'], t),
|
||||||
const prefix = conditions.length
|
)
|
||||||
? conditions.join(rule.join === 'or' ? ' veya ' : ' ve ')
|
.filter((text) => text !== always)
|
||||||
: 'Her zaman'
|
const joiner =
|
||||||
|
rule.join === 'or'
|
||||||
|
? ` ${t('::ListForms.ScriptBuilder.Word.Or')} `
|
||||||
|
: ` ${t('::ListForms.ScriptBuilder.Word.And')} `
|
||||||
|
const prefix = conditions.length ? conditions.join(joiner) : always
|
||||||
return `${prefix} → ${action}`
|
return `${prefix} → ${action}`
|
||||||
},
|
},
|
||||||
getTrigger: (rule) => effectiveTrigger(asFormRule(rule)),
|
getTrigger: (rule) => effectiveTrigger(asFormRule(rule)),
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,17 @@ export type RuleCondition = {
|
||||||
export type RuleTrigger = 'change' | 'open' | 'both'
|
export type RuleTrigger = 'change' | 'open' | 'both'
|
||||||
|
|
||||||
export const triggerLabels: { value: RuleTrigger; label: string; help: string }[] = [
|
export const triggerLabels: { value: RuleTrigger; label: string; help: string }[] = [
|
||||||
{ value: 'change', label: 'Değiştiğinde', help: 'Yalnızca bu alanın değeri değişince çalışır.' },
|
{
|
||||||
|
value: 'change',
|
||||||
|
label: 'ListForms.ScriptBuilder.Change.Label',
|
||||||
|
help: 'ListForms.ScriptBuilder.Change.Help',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'open',
|
value: 'open',
|
||||||
label: 'Form açılırken',
|
label: 'ListForms.ScriptBuilder.Open.Label',
|
||||||
help: 'Popup edit form açıldığında bir kez çalışır. Varsayılan değer üretmek için.',
|
help: 'ListForms.ScriptBuilder.Open.Help',
|
||||||
},
|
},
|
||||||
{ value: 'both', label: 'Her ikisi', help: 'Hem açılışta hem değer değişiminde çalışır.' },
|
{ value: 'both', label: 'Her ikisi', help: 'ListForms.ScriptBuilder.Both.Help' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** Ek koşullar nasıl birleşecek. */
|
/** Ek koşullar nasıl birleşecek. */
|
||||||
|
|
@ -93,7 +97,7 @@ export type Recipe = {
|
||||||
conditionIsArgument?: boolean
|
conditionIsArgument?: boolean
|
||||||
params: RecipeParam[]
|
params: RecipeParam[]
|
||||||
build: (rule: ScriptRule) => string
|
build: (rule: ScriptRule) => string
|
||||||
describe: (rule: ScriptRule) => string
|
describe: (rule: ScriptRule, t: TranslateFn) => string
|
||||||
example: string
|
example: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,17 +121,31 @@ export type RecipeId =
|
||||||
| 'custom'
|
| 'custom'
|
||||||
|
|
||||||
export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [
|
export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [
|
||||||
{ key: 'calc', title: 'Hesaplama', description: 'Alanlardan yeni bir değer üretir.' },
|
{ key: 'calc', title: 'Hesaplama', description: 'ListForms.ScriptBuilder.Calc.Description' },
|
||||||
{ key: 'data', title: 'Veri', description: 'Alanlara değer yazar, kopyalar veya temizler.' },
|
{ key: 'data', title: 'Veri', description: 'ListForms.ScriptBuilder.Data.Description' },
|
||||||
{ key: 'view', title: 'Görünüm', description: 'Alanların düzenlenebilirliğini yönetir.' },
|
{
|
||||||
{ key: 'interaction', title: 'Etkileşim', description: 'Kullanıcıya mesaj gösterir, sayfa açar.' },
|
key: 'view',
|
||||||
{ key: 'integration', title: 'Entegrasyon', description: 'API ve servis çağrıları, serbest kod.' },
|
title: 'ListForms.ScriptBuilder.View.Title',
|
||||||
|
description: 'ListForms.ScriptBuilder.View.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'interaction',
|
||||||
|
title: 'ListForms.ScriptBuilder.Interaction.Title',
|
||||||
|
description: 'ListForms.ScriptBuilder.Interaction.Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'integration',
|
||||||
|
title: 'Entegrasyon',
|
||||||
|
description: 'ListForms.ScriptBuilder.Integration.Description',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ------------------------------------------------------------------ *
|
||||||
* Yardımcılar
|
* Yardımcılar
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
export type TranslateFn = (key: string, params?: Record<string, string | number>) => string
|
||||||
|
|
||||||
const q = (input: string) => JSON.stringify(input ?? '')
|
const q = (input: string) => JSON.stringify(input ?? '')
|
||||||
|
|
||||||
const label = (input: string, fallback: string) => input || fallback
|
const label = (input: string, fallback: string) => input || fallback
|
||||||
|
|
@ -172,28 +190,31 @@ export const buildRuleConditionExpression = (rule: ScriptRule): string => {
|
||||||
return parts.map((part) => `(${part})`).join(rule.join === 'or' ? ' || ' : ' && ')
|
return parts.map((part) => `(${part})`).join(rule.join === 'or' ? ' || ' : ' && ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const describeCondition = (condition: RuleCondition): string => {
|
export const describeCondition = (condition: RuleCondition, t: TranslateFn): string => {
|
||||||
if (condition.operator === 'always' || !condition.source) return 'Her zaman'
|
if (condition.operator === 'always' || !condition.source) {
|
||||||
const source = label(condition.source, 'alan')
|
return t('::ListForms.ScriptBuilder.Describe.Always')
|
||||||
|
}
|
||||||
|
const source = label(condition.source, t('::ListForms.ScriptBuilder.Word.Field'))
|
||||||
|
const value = label(condition.value, t('::ListForms.ScriptBuilder.Word.Value'))
|
||||||
switch (condition.operator) {
|
switch (condition.operator) {
|
||||||
case 'empty':
|
case 'empty':
|
||||||
return `${source} boşsa`
|
return t('::ListForms.ScriptBuilder.Describe.Empty', { source })
|
||||||
case 'notEmpty':
|
case 'notEmpty':
|
||||||
return `${source} doluysa`
|
return t('::ListForms.ScriptBuilder.Describe.NotEmpty', { source })
|
||||||
case 'isTrue':
|
case 'isTrue':
|
||||||
return `${source} işaretliyse`
|
return t('::ListForms.ScriptBuilder.Describe.IsTrue', { source })
|
||||||
case 'isFalse':
|
case 'isFalse':
|
||||||
return `${source} işaretli değilse`
|
return t('::ListForms.ScriptBuilder.Describe.IsFalse', { source })
|
||||||
case 'contains':
|
case 'contains':
|
||||||
return `${source} içinde "${label(condition.value, 'değer')}" geçiyorsa`
|
return t('::ListForms.ScriptBuilder.Describe.Contains', { source, value })
|
||||||
case 'greaterThan':
|
case 'greaterThan':
|
||||||
return `${source} > ${label(condition.value, 'değer')} ise`
|
return t('::ListForms.ScriptBuilder.Describe.GreaterThan', { source, value })
|
||||||
case 'lessThan':
|
case 'lessThan':
|
||||||
return `${source} < ${label(condition.value, 'değer')} ise`
|
return t('::ListForms.ScriptBuilder.Describe.LessThan', { source, value })
|
||||||
case 'notEquals':
|
case 'notEquals':
|
||||||
return `${source} ≠ "${label(condition.value, 'değer')}" ise`
|
return t('::ListForms.ScriptBuilder.Describe.NotEquals', { source, value })
|
||||||
default:
|
default:
|
||||||
return `${source} = "${label(condition.value, 'değer')}" ise`
|
return t('::ListForms.ScriptBuilder.Describe.Equals', { source, value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,81 +227,95 @@ const numberOrField = (raw: string) => (isNumericLiteral(raw) ? raw : `num(${q(r
|
||||||
export const recipes: Recipe[] = [
|
export const recipes: Recipe[] = [
|
||||||
{
|
{
|
||||||
id: 'multiply',
|
id: 'multiply',
|
||||||
label: 'Çarpım (miktar × fiyat)',
|
label: 'ListForms.ScriptBuilder.Multiply.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'İki alanı çarpar, yuvarlar ve hedefe yazar.',
|
summary: 'ListForms.ScriptBuilder.Multiply.Summary',
|
||||||
example: "set('TotalAmount', round(num('Quantity') * num('UnitPrice'), 2))",
|
example: "set('TotalAmount', round(num('Quantity') * num('UnitPrice'), 2))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'left', label: 'Birinci alan', type: 'field' },
|
{ key: 'left', label: 'Birinci alan', type: 'field' },
|
||||||
{ key: 'right', label: 'İkinci alan', type: 'field' },
|
{ key: 'right', label: 'ListForms.ScriptBuilder.Right.Label', type: 'field' },
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'digits',
|
key: 'digits',
|
||||||
label: 'Ondalık',
|
label: 'ListForms.ScriptBuilder.Digits.Label',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true,
|
optional: true,
|
||||||
placeholder: '2',
|
placeholder: '2',
|
||||||
help: 'Boş bırakılırsa 2 kullanılır.',
|
help: 'ListForms.ScriptBuilder.Digits.Help',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) =>
|
build: ({ params }) =>
|
||||||
`set(${q(params.target)}, round(${numberOrField(params.left)} * ${numberOrField(
|
`set(${q(params.target)}, round(${numberOrField(params.left)} * ${numberOrField(
|
||||||
params.right,
|
params.right,
|
||||||
)}, ${params.digits || 2}))`,
|
)}, ${params.digits || 2}))`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.left, 'alan')} × ${label(params.right, 'alan')} sonucunu ${label(
|
t('::ListForms.ScriptBuilder.Describe.Multiply', {
|
||||||
params.target,
|
left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
|
||||||
'hedef',
|
right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
|
||||||
)} alanına yaz.`,
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'subtract',
|
id: 'subtract',
|
||||||
label: 'Fark (brüt − indirim)',
|
label: 'ListForms.ScriptBuilder.Subtract.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'Birinci alandan ikinciyi çıkarır, yuvarlar ve hedefe yazar.',
|
summary: 'ListForms.ScriptBuilder.Subtract.Summary',
|
||||||
example: "set('Net', round(num('Gross') - num('Discount'), 2))",
|
example: "set('Net', round(num('Gross') - num('Discount'), 2))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'left', label: 'Çıkarılan alan', type: 'field' },
|
{ key: 'left', label: 'ListForms.ScriptBuilder.Left.Label', type: 'field' },
|
||||||
{ key: 'right', label: 'Çıkan alan', type: 'field' },
|
{ key: 'right', label: 'ListForms.ScriptBuilder.Right.Label2', type: 'field' },
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
|
||||||
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
|
{
|
||||||
|
key: 'digits',
|
||||||
|
label: 'ListForms.ScriptBuilder.Digits.Label',
|
||||||
|
type: 'number',
|
||||||
|
optional: true,
|
||||||
|
placeholder: '2',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) =>
|
build: ({ params }) =>
|
||||||
`set(${q(params.target)}, round(${numberOrField(params.left)} - ${numberOrField(
|
`set(${q(params.target)}, round(${numberOrField(params.left)} - ${numberOrField(
|
||||||
params.right,
|
params.right,
|
||||||
)}, ${params.digits || 2}))`,
|
)}, ${params.digits || 2}))`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.left, 'alan')} − ${label(params.right, 'alan')} sonucunu ${label(
|
t('::ListForms.ScriptBuilder.Describe.Subtract', {
|
||||||
params.target,
|
left: label(params.left, t('::ListForms.ScriptBuilder.Word.Field')),
|
||||||
'hedef',
|
right: label(params.right, t('::ListForms.ScriptBuilder.Word.Field')),
|
||||||
)} alanına yaz.`,
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'percent',
|
id: 'percent',
|
||||||
label: 'Yüzde (KDV, iskonto)',
|
label: 'ListForms.ScriptBuilder.Percent.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'Tutarın belirtilen yüzdesini hesaplar; oran alan ya da sabit olabilir.',
|
summary: 'ListForms.ScriptBuilder.Percent.Summary',
|
||||||
example: "set('Kdv', round(num('Tutar') * num('KdvOrani') / 100, 2))",
|
example: "set('Kdv', round(num('Tutar') * num('KdvOrani') / 100, 2))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'amount', label: 'Tutar alanı', type: 'field' },
|
{ key: 'amount', label: 'ListForms.ScriptBuilder.Amount.Label', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'rate',
|
key: 'rate',
|
||||||
label: 'Oran',
|
label: 'Oran',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: '20 veya KdvOrani',
|
placeholder: '20 veya KdvOrani',
|
||||||
help: 'Sayı yazarsan sabit oran, alan adı yazarsan o alandan okunur.',
|
help: 'ListForms.ScriptBuilder.Rate.Help',
|
||||||
|
},
|
||||||
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label3', type: 'field' },
|
||||||
|
{
|
||||||
|
key: 'digits',
|
||||||
|
label: 'ListForms.ScriptBuilder.Digits.Label2',
|
||||||
|
type: 'number',
|
||||||
|
optional: true,
|
||||||
|
placeholder: '2',
|
||||||
},
|
},
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
|
||||||
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
|
|
||||||
{
|
{
|
||||||
key: 'mode',
|
key: 'mode',
|
||||||
label: 'Sonuç',
|
label: 'ListForms.ScriptBuilder.Mode.Label',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
optional: true,
|
optional: true,
|
||||||
choices: [
|
choices: [
|
||||||
{ value: '', label: 'Yalnızca yüzde tutarı' },
|
{ value: '', label: 'ListForms.ScriptBuilder.X.Label' },
|
||||||
{ value: 'add', label: 'Tutar + yüzde' },
|
{ value: 'add', label: 'ListForms.ScriptBuilder.Add.Label' },
|
||||||
{ value: 'subtract', label: 'Tutar − yüzde' },
|
{ value: 'subtract', label: 'ListForms.ScriptBuilder.Subtract.Label' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -295,191 +330,215 @@ export const recipes: Recipe[] = [
|
||||||
: portion
|
: portion
|
||||||
return `set(${q(params.target)}, round(${expression}, ${params.digits || 2}))`
|
return `set(${q(params.target)}, round(${expression}, ${params.digits || 2}))`
|
||||||
},
|
},
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.amount, 'tutar')} üzerinden %${label(params.rate, 'oran')} hesabını ${label(
|
t('::ListForms.ScriptBuilder.Describe.Percent', {
|
||||||
params.target,
|
amount: label(params.amount, t('::ListForms.ScriptBuilder.Word.Amount')),
|
||||||
'hedef',
|
rate: label(params.rate, t('::ListForms.ScriptBuilder.Word.Rate')),
|
||||||
)} alanına yaz.`,
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'today',
|
id: 'today',
|
||||||
label: 'Bugünün tarihi',
|
label: 'ListForms.ScriptBuilder.Today.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'Bugünü (veya kaç gün sonrasını) tarih alanına yazar.',
|
summary: 'ListForms.ScriptBuilder.Today.Summary',
|
||||||
example: "set('Tarih', new Date().toISOString().slice(0, 10))",
|
example: "set('Tarih', new Date().toISOString().slice(0, 10))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Hedef alan', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'offset',
|
key: 'offset',
|
||||||
label: 'Gün ekle',
|
label: 'ListForms.ScriptBuilder.Offset.Label',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true,
|
optional: true,
|
||||||
placeholder: '0',
|
placeholder: '0',
|
||||||
help: 'Negatif yazarsan geçmiş bir tarih üretir.',
|
help: 'ListForms.ScriptBuilder.Offset.Help',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) => {
|
build: ({ params }) => {
|
||||||
const offset = Number(params.offset) || 0
|
const offset = Number(params.offset) || 0
|
||||||
const base = offset
|
const base = offset ? `new Date(Date.now() + ${offset} * 86400000)` : 'new Date()'
|
||||||
? `new Date(Date.now() + ${offset} * 86400000)`
|
|
||||||
: 'new Date()'
|
|
||||||
return `set(${q(params.target)}, ${base}.toISOString().slice(0, 10))`
|
return `set(${q(params.target)}, ${base}.toISOString().slice(0, 10))`
|
||||||
},
|
},
|
||||||
describe: ({ params }) => {
|
describe: ({ params }, t) => {
|
||||||
const offset = Number(params.offset) || 0
|
const offset = Number(params.offset) || 0
|
||||||
const when = offset > 0 ? `${offset} gün sonrasını` : offset < 0 ? `${-offset} gün öncesini` : 'bugünü'
|
const when =
|
||||||
return `${when} ${label(params.target, 'hedef')} alanına yaz.`
|
offset > 0
|
||||||
|
? t('::ListForms.ScriptBuilder.Word.DaysLater', { count: offset })
|
||||||
|
: offset < 0
|
||||||
|
? t('::ListForms.ScriptBuilder.Word.DaysEarlier', { count: -offset })
|
||||||
|
: t('::ListForms.ScriptBuilder.Word.Today')
|
||||||
|
return t('::ListForms.ScriptBuilder.Describe.Today', {
|
||||||
|
when,
|
||||||
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sum',
|
id: 'sum',
|
||||||
label: 'Toplama',
|
label: 'Toplama',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'Seçilen alanları toplar ve hedefe yazar.',
|
summary: 'ListForms.ScriptBuilder.Sum.Summary',
|
||||||
example: "set('Total', round(sum('Net', 'Tax'), 2))",
|
example: "set('Total', round(sum('Net', 'Tax'), 2))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label4', type: 'field' },
|
||||||
{ key: 'sources', label: 'Toplanacak alanlar', type: 'fieldList' },
|
{ key: 'sources', label: 'ListForms.ScriptBuilder.Sources.Label3', type: 'fieldList' },
|
||||||
{ key: 'digits', label: 'Ondalık', type: 'number', optional: true, placeholder: '2' },
|
{
|
||||||
|
key: 'digits',
|
||||||
|
label: 'ListForms.ScriptBuilder.Digits.Label3',
|
||||||
|
type: 'number',
|
||||||
|
optional: true,
|
||||||
|
placeholder: '2',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
build: ({ params, fields }) =>
|
build: ({ params, fields }) =>
|
||||||
`set(${q(params.target)}, round(sum(${(fields ?? []).map(q).join(', ')}), ${
|
`set(${q(params.target)}, round(sum(${(fields ?? []).map(q).join(', ')}), ${
|
||||||
params.digits || 2
|
params.digits || 2
|
||||||
}))`,
|
}))`,
|
||||||
describe: ({ params, fields }) =>
|
describe: ({ params, fields }, t) =>
|
||||||
`${(fields ?? []).join(' + ') || 'alanlar'} toplamını ${label(
|
t('::ListForms.ScriptBuilder.Describe.Sum', {
|
||||||
params.target,
|
sources: (fields ?? []).join(' + ') || t('::ListForms.ScriptBuilder.Word.Fields'),
|
||||||
'hedef',
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
)} alanına yaz.`,
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'formula',
|
id: 'formula',
|
||||||
label: 'Serbest formül',
|
label: 'ListForms.ScriptBuilder.Formula.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'JavaScript ifadesi yazarsın; num/str/get/selected kullanabilirsin.',
|
summary: 'ListForms.ScriptBuilder.Formula.Summary',
|
||||||
example: "set('Net', num('Gross') - num('Discount'))",
|
example: "set('Net', num('Gross') - num('Discount'))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label5', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'expression',
|
key: 'expression',
|
||||||
label: 'İfade',
|
label: 'ListForms.ScriptBuilder.Expression.Label',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: "num('Gross') * 0.18",
|
placeholder: "num('Gross') * 0.18",
|
||||||
help: "Kullanılabilir: num('X'), str('X'), get('X'), value, selected('Yol'), round(x, 2)",
|
help: 'ListForms.ScriptBuilder.Expression.Help',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) => `set(${q(params.target)}, ${params.expression || 'null'})`,
|
build: ({ params }) => `set(${q(params.target)}, ${params.expression || 'null'})`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.expression, 'ifade')} sonucunu ${label(params.target, 'hedef')} alanına yaz.`,
|
t('::ListForms.ScriptBuilder.Describe.Formula', {
|
||||||
|
expression: label(params.expression, t('::ListForms.ScriptBuilder.Word.Expression')),
|
||||||
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'days',
|
id: 'days',
|
||||||
label: 'Gün farkı',
|
label: 'ListForms.ScriptBuilder.Days.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'İki tarih arasındaki gün sayısını (bitiş dahil) yazar.',
|
summary: 'ListForms.ScriptBuilder.Days.Summary',
|
||||||
example: "set('TotalDays', days('StartDate', 'EndDate'))",
|
example: "set('TotalDays', days('StartDate', 'EndDate'))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'start', label: 'Başlangıç tarihi', type: 'field' },
|
{ key: 'start', label: 'ListForms.ScriptBuilder.Start.Label', type: 'field' },
|
||||||
{ key: 'end', label: 'Bitiş tarihi', type: 'field' },
|
{ key: 'end', label: 'ListForms.ScriptBuilder.End.Label', type: 'field' },
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label6', type: 'field' },
|
||||||
],
|
],
|
||||||
build: ({ params }) =>
|
build: ({ params }) => `set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`,
|
||||||
`set(${q(params.target)}, days(${q(params.start)}, ${q(params.end)}))`,
|
describe: ({ params }, t) =>
|
||||||
describe: ({ params }) =>
|
t('::ListForms.ScriptBuilder.Describe.Days', {
|
||||||
`${label(params.start, 'başlangıç')} → ${label(params.end, 'bitiş')} gün farkını ${label(
|
start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
|
||||||
params.target,
|
end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
|
||||||
'hedef',
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
)} alanına yaz.`,
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hours',
|
id: 'hours',
|
||||||
label: 'Saat farkı',
|
label: 'ListForms.ScriptBuilder.Hours.Label',
|
||||||
group: 'calc',
|
group: 'calc',
|
||||||
summary: 'İki saat arasındaki farkı yazar; bitiş küçükse ertesi güne taşar.',
|
summary: 'ListForms.ScriptBuilder.Hours.Summary',
|
||||||
example: "set('TotalHours', hours('StartTime', 'EndTime'))",
|
example: "set('TotalHours', hours('StartTime', 'EndTime'))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'start', label: 'Başlangıç saati', type: 'field' },
|
{ key: 'start', label: 'ListForms.ScriptBuilder.Start.Label2', type: 'field' },
|
||||||
{ key: 'end', label: 'Bitiş saati', type: 'field' },
|
{ key: 'end', label: 'ListForms.ScriptBuilder.End.Label2', type: 'field' },
|
||||||
{ key: 'target', label: 'Sonuç alanı', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label7', type: 'field' },
|
||||||
],
|
],
|
||||||
build: ({ params }) =>
|
build: ({ params }) => `set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`,
|
||||||
`set(${q(params.target)}, hours(${q(params.start)}, ${q(params.end)}))`,
|
describe: ({ params }, t) =>
|
||||||
describe: ({ params }) =>
|
t('::ListForms.ScriptBuilder.Describe.Hours', {
|
||||||
`${label(params.start, 'başlangıç')} → ${label(params.end, 'bitiş')} saat farkını ${label(
|
start: label(params.start, t('::ListForms.ScriptBuilder.Word.Start')),
|
||||||
params.target,
|
end: label(params.end, t('::ListForms.ScriptBuilder.Word.End')),
|
||||||
'hedef',
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
)} alanına yaz.`,
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'copy',
|
id: 'copy',
|
||||||
label: 'Seçili kayıttan kopyala',
|
label: 'ListForms.ScriptBuilder.Copy.Label',
|
||||||
group: 'data',
|
group: 'data',
|
||||||
summary: 'Lookup/GridBox ile seçilen kaydın bir kolonunu form alanına taşır.',
|
summary: 'ListForms.ScriptBuilder.Copy.Summary',
|
||||||
example: "copy('unitPrice', 'UnitPrice')",
|
example: "copy('unitPrice', 'UnitPrice')",
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
key: 'source',
|
key: 'source',
|
||||||
label: 'Seçili kayıttaki kolon/yol',
|
label: 'ListForms.ScriptBuilder.Source.Label',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'unitPrice veya Customer.Name',
|
placeholder: 'unitPrice veya Customer.Name',
|
||||||
},
|
},
|
||||||
{ key: 'target', label: 'Hedef alan', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
|
||||||
],
|
],
|
||||||
build: ({ params }) => `copy(${q(params.source)}, ${q(params.target)})`,
|
build: ({ params }) => `copy(${q(params.source)}, ${q(params.target)})`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`Seçili kayıttaki ${label(params.source, 'kolon')} değerini ${label(
|
t('::ListForms.ScriptBuilder.Describe.Copy', {
|
||||||
params.target,
|
source: label(params.source, t('::ListForms.ScriptBuilder.Word.Column')),
|
||||||
'hedef',
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
)} alanına kopyala.`,
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'setValue',
|
id: 'setValue',
|
||||||
label: 'Değer yaz',
|
label: 'ListForms.ScriptBuilder.SetValue.Label',
|
||||||
group: 'data',
|
group: 'data',
|
||||||
summary: 'Sabit metin veya {Alan} / {value} / {selected.Yol} token’ları yazar.',
|
summary: 'ListForms.ScriptBuilder.SetValue.Summary',
|
||||||
example: "set('Aciklama', tpl('{Musteri} - {value}'))",
|
example: "set('Aciklama', tpl('{Musteri} - {value}'))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Hedef alan', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'text',
|
key: 'text',
|
||||||
label: 'Değer',
|
label: 'ListForms.ScriptBuilder.Text.Label',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: '{Musteri} - {value}',
|
placeholder: '{Musteri} - {value}',
|
||||||
help: 'Token kullanabilirsin: {AlanAdi}, {value}, {selected.Name}',
|
help: 'ListForms.ScriptBuilder.Text.Help',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`,
|
build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.target, 'hedef')} alanına "${label(params.text, 'değer')}" yaz.`,
|
t('::ListForms.ScriptBuilder.Describe.SetValue', {
|
||||||
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
|
text: label(params.text, t('::ListForms.ScriptBuilder.Word.Value')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'clear',
|
id: 'clear',
|
||||||
label: 'Alanları temizle',
|
label: 'ListForms.ScriptBuilder.Clear.Label',
|
||||||
group: 'data',
|
group: 'data',
|
||||||
summary: 'Seçilen alanları null yapar.',
|
summary: 'ListForms.ScriptBuilder.Clear.Summary',
|
||||||
example: "clear('Il', 'Ilce')",
|
example: "clear('Il', 'Ilce')",
|
||||||
params: [{ key: 'sources', label: 'Temizlenecek alanlar', type: 'fieldList' }],
|
params: [
|
||||||
|
{ key: 'sources', label: 'ListForms.ScriptBuilder.Sources.Label2', type: 'fieldList' },
|
||||||
|
],
|
||||||
build: ({ fields }) => `clear(${(fields ?? []).map(q).join(', ')})`,
|
build: ({ fields }) => `clear(${(fields ?? []).map(q).join(', ')})`,
|
||||||
describe: ({ fields }) => `${(fields ?? []).join(', ') || 'alanları'} temizle.`,
|
describe: ({ fields }, t) =>
|
||||||
|
t('::ListForms.ScriptBuilder.Describe.Clear', {
|
||||||
|
sources: (fields ?? []).join(', ') || t('::ListForms.ScriptBuilder.Word.Fields'),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'readOnly',
|
id: 'readOnly',
|
||||||
label: 'Salt okunur yap',
|
label: 'Salt okunur yap',
|
||||||
group: 'view',
|
group: 'view',
|
||||||
summary: 'Koşul sağlandığında alanı kilitler, sağlanmadığında açar.',
|
summary: 'ListForms.ScriptBuilder.ReadOnly.Summary',
|
||||||
conditionIsArgument: true,
|
conditionIsArgument: true,
|
||||||
example: "readOnly('SatisSebebi', str('Durum') === 'Aktif')",
|
example: "readOnly('SatisSebebi', str('Durum') === 'Aktif')",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Kilitlenecek alan', type: 'field' },
|
{ key: 'target', label: 'Kilitlenecek alan', type: 'field' },
|
||||||
{
|
{
|
||||||
key: 'invert',
|
key: 'invert',
|
||||||
label: 'Tersine çevir',
|
label: 'ListForms.ScriptBuilder.Invert.Label',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
optional: true,
|
optional: true,
|
||||||
help: 'Evet seçilirse koşul sağlandığında alan açılır.',
|
help: 'ListForms.ScriptBuilder.Invert.Help',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: '', label: 'Hayır' },
|
{ value: '', label: 'ListForms.ScriptBuilder.X.Label2' },
|
||||||
{ value: 'true', label: 'Evet' },
|
{ value: 'true', label: 'ListForms.ScriptBuilder.Yes' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -488,39 +547,62 @@ export const recipes: Recipe[] = [
|
||||||
const flag = rule.params.invert === 'true' ? `!(${expression})` : expression
|
const flag = rule.params.invert === 'true' ? `!(${expression})` : expression
|
||||||
return `readOnly(${q(rule.params.target)}, ${flag})`
|
return `readOnly(${q(rule.params.target)}, ${flag})`
|
||||||
},
|
},
|
||||||
describe: ({ params, condition }) =>
|
describe: ({ params, condition }, t) =>
|
||||||
`${describeCondition(condition)} ${label(params.target, 'alan')} alanını ${
|
t('::ListForms.ScriptBuilder.Describe.ReadOnly', {
|
||||||
params.invert === 'true' ? 'düzenlenebilir' : 'salt okunur'
|
condition: describeCondition(condition, t),
|
||||||
} yap.`,
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Field')),
|
||||||
|
mode: t(
|
||||||
|
params.invert === 'true'
|
||||||
|
? '::ListForms.ScriptBuilder.Word.Editable'
|
||||||
|
: '::ListForms.ScriptBuilder.Word.ReadOnly',
|
||||||
|
),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notify',
|
id: 'notify',
|
||||||
label: 'Uyarı göster',
|
label: 'ListForms.ScriptBuilder.Notify.Label',
|
||||||
group: 'interaction',
|
group: 'interaction',
|
||||||
summary: 'Kullanıcıya bilgi mesajı gösterir.',
|
summary: 'ListForms.ScriptBuilder.Notify.Summary',
|
||||||
example: "notify('Limit aşıldı')",
|
example: "notify('Limit aşıldı')",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'message', label: 'Mesaj', type: 'text', placeholder: '{Musteri} limiti aşıldı' },
|
{
|
||||||
|
key: 'message',
|
||||||
|
label: 'ListForms.ScriptBuilder.Message.Label',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'ListForms.ScriptBuilder.Notify.MessagePlaceholder',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) => `notify(${q(params.message)})`,
|
build: ({ params }) => `notify(${q(params.message)})`,
|
||||||
describe: ({ params }) => `"${label(params.message, 'mesaj')}" uyarısını göster.`,
|
describe: ({ params }, t) =>
|
||||||
|
t('::ListForms.ScriptBuilder.Describe.Notify', {
|
||||||
|
message: label(params.message, t('::ListForms.ScriptBuilder.Word.Message')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ask',
|
id: 'ask',
|
||||||
label: 'Onay iste',
|
label: 'ListForms.ScriptBuilder.Ask.Label',
|
||||||
group: 'interaction',
|
group: 'interaction',
|
||||||
summary: 'Onay ister; kullanıcı vazgeçerse alan eski değerine döner.',
|
summary: 'ListForms.ScriptBuilder.Ask.Summary',
|
||||||
example: "if (!ask('Emin misiniz?')) return",
|
example: "if (!ask('Emin misiniz?')) return",
|
||||||
params: [{ key: 'message', label: 'Mesaj', type: 'text', placeholder: 'Emin misiniz?' }],
|
params: [
|
||||||
|
{
|
||||||
|
key: 'message',
|
||||||
|
label: 'ListForms.ScriptBuilder.Message.Label',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'ListForms.ScriptBuilder.Ask.MessagePlaceholder',
|
||||||
|
},
|
||||||
|
],
|
||||||
build: ({ params }) => `if (!ask(${q(params.message)})) return`,
|
build: ({ params }) => `if (!ask(${q(params.message)})) return`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`"${label(params.message, 'mesaj')}" onayını iste; vazgeçilirse değeri geri al.`,
|
t('::ListForms.ScriptBuilder.Describe.Ask', {
|
||||||
|
message: label(params.message, t('::ListForms.ScriptBuilder.Word.Message')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'openUrl',
|
id: 'openUrl',
|
||||||
label: 'URL aç',
|
label: 'ListForms.ScriptBuilder.OpenUrl.Label',
|
||||||
group: 'interaction',
|
group: 'interaction',
|
||||||
summary: 'Yeni sekmede veya aynı sekmede adres açar.',
|
summary: 'ListForms.ScriptBuilder.OpenUrl.Summary',
|
||||||
example: "openUrl('/report?id={Id}')",
|
example: "openUrl('/report?id={Id}')",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'url', label: 'URL', type: 'text', placeholder: '/report?id={Id}' },
|
{ key: 'url', label: 'URL', type: 'text', placeholder: '/report?id={Id}' },
|
||||||
|
|
@ -539,16 +621,19 @@ export const recipes: Recipe[] = [
|
||||||
params.target && params.target !== '_blank'
|
params.target && params.target !== '_blank'
|
||||||
? `openUrl(${q(params.url)}, ${q(params.target)})`
|
? `openUrl(${q(params.url)}, ${q(params.target)})`
|
||||||
: `openUrl(${q(params.url)})`,
|
: `openUrl(${q(params.url)})`,
|
||||||
describe: ({ params }) => `${label(params.url, 'adres')} adresini aç.`,
|
describe: ({ params }, t) =>
|
||||||
|
t('::ListForms.ScriptBuilder.Describe.OpenUrl', {
|
||||||
|
url: label(params.url, t('::ListForms.ScriptBuilder.Word.Address')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'apiToField',
|
id: 'apiToField',
|
||||||
label: 'API sonucunu alana yaz',
|
label: 'API sonucunu alana yaz',
|
||||||
group: 'integration',
|
group: 'integration',
|
||||||
summary: 'Adresi çağırır, JSON içinden okuduğu değeri alana yazar.',
|
summary: 'ListForms.ScriptBuilder.ApiToField.Summary',
|
||||||
example: "set('Unvan', await api('/api/musteri/{value}', 'data.name'))",
|
example: "set('Unvan', await api('/api/musteri/{value}', 'data.name'))",
|
||||||
params: [
|
params: [
|
||||||
{ key: 'target', label: 'Hedef alan', type: 'field' },
|
{ key: 'target', label: 'ListForms.ScriptBuilder.Target.Label2', type: 'field' },
|
||||||
{ key: 'url', label: 'API adresi', type: 'text', placeholder: '/api/musteri/{value}' },
|
{ key: 'url', label: 'API adresi', type: 'text', placeholder: '/api/musteri/{value}' },
|
||||||
{
|
{
|
||||||
key: 'path',
|
key: 'path',
|
||||||
|
|
@ -556,24 +641,24 @@ export const recipes: Recipe[] = [
|
||||||
type: 'text',
|
type: 'text',
|
||||||
optional: true,
|
optional: true,
|
||||||
placeholder: 'data.name',
|
placeholder: 'data.name',
|
||||||
help: 'Boş bırakılırsa tüm JSON yazılır.',
|
help: 'ListForms.ScriptBuilder.Path.Help',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) =>
|
build: ({ params }) =>
|
||||||
`set(${q(params.target)}, await api(${q(params.url)}${
|
`set(${q(params.target)}, await api(${q(params.url)}${
|
||||||
params.path ? `, ${q(params.path)}` : ''
|
params.path ? `, ${q(params.path)}` : ''
|
||||||
}))`,
|
}))`,
|
||||||
describe: ({ params }) =>
|
describe: ({ params }, t) =>
|
||||||
`${label(params.url, 'api')} çağrısının sonucunu ${label(
|
t('::ListForms.ScriptBuilder.Describe.ApiToField', {
|
||||||
params.target,
|
url: label(params.url, 'api'),
|
||||||
'hedef',
|
target: label(params.target, t('::ListForms.ScriptBuilder.Word.Target')),
|
||||||
)} alanına yaz.`,
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'custom',
|
id: 'custom',
|
||||||
label: 'Serbest tek satır',
|
label: 'ListForms.ScriptBuilder.Custom.Label',
|
||||||
group: 'integration',
|
group: 'integration',
|
||||||
summary: 'Servis çağrısı veya kendi yazdığın tek satırlık kod.',
|
summary: 'ListForms.ScriptBuilder.Custom.Summary',
|
||||||
example: 'UiEvalService.ApiGenerateBackgroundWorkers()',
|
example: 'UiEvalService.ApiGenerateBackgroundWorkers()',
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
@ -584,7 +669,10 @@ export const recipes: Recipe[] = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
build: ({ params }) => (params.code || '').trim().replace(/;$/, ''),
|
build: ({ params }) => (params.code || '').trim().replace(/;$/, ''),
|
||||||
describe: ({ params }) => label(params.code, 'kod') + ' çalıştır.',
|
describe: ({ params }, t) =>
|
||||||
|
t('::ListForms.ScriptBuilder.Describe.Custom', {
|
||||||
|
code: label(params.code, t('::ListForms.ScriptBuilder.Word.Code')),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -741,25 +829,36 @@ export const createRule = (recipe: RecipeId): ScriptRule => ({
|
||||||
|
|
||||||
/** Cheatsheet olarak gösterilen runtime API özeti. */
|
/** Cheatsheet olarak gösterilen runtime API özeti. */
|
||||||
export const runtimeApiReference: { signature: string; description: string }[] = [
|
export const runtimeApiReference: { signature: string; description: string }[] = [
|
||||||
{ signature: 'value / field', description: 'Değişen alanın değeri ve adı.' },
|
{ signature: 'value / field', description: 'ListForms.ScriptBuilder.Api.ValueField' },
|
||||||
{ signature: "get('Alan')", description: 'Form değerini okur (büyük/küçük harf duyarsız).' },
|
{ signature: "get('Alan')", description: 'ListForms.ScriptBuilder.Api.Get' },
|
||||||
{ signature: "num('Alan') / str('Alan') / bool('Alan')", description: 'Tip dönüşümlü okuma.' },
|
{
|
||||||
{ signature: "dateOf('Alan')", description: 'Date nesnesi ya da null döner.' },
|
signature: "num('Alan') / str('Alan') / bool('Alan')",
|
||||||
{ signature: "set('Alan', deger)", description: 'Alana yazar. Nesne de verebilirsin.' },
|
description: 'ListForms.ScriptBuilder.Api.Cast',
|
||||||
{ signature: "set({ A: 1, B: 2 })", description: 'Birden fazla alanı tek çağrıda yazar.' },
|
},
|
||||||
{ signature: "clear('A', 'B')", description: 'Alanları null yapar.' },
|
{ signature: "dateOf('Alan')", description: 'ListForms.ScriptBuilder.Api.DateOf' },
|
||||||
{ signature: "copy('kolon', 'Alan')", description: 'Seçili lookup kaydından kopyalar.' },
|
{ signature: "set('Alan', deger)", description: 'ListForms.ScriptBuilder.Api.Set' },
|
||||||
{ signature: "selected('Yol') / snum() / sstr()", description: 'Seçili kaydı okur.' },
|
{ signature: 'set({ A: 1, B: 2 })', description: 'ListForms.ScriptBuilder.Api.SetMany' },
|
||||||
{ signature: "readOnly('Alan', kosul)", description: 'Alanı kilitler veya açar.' },
|
{ signature: "clear('A', 'B')", description: 'ListForms.ScriptBuilder.Api.Clear' },
|
||||||
{ signature: "round(x, 2) / sum('A','B')", description: 'Yuvarlama ve toplama.' },
|
{ signature: "copy('kolon', 'Alan')", description: 'ListForms.ScriptBuilder.Api.Copy' },
|
||||||
{ signature: "days('Bas','Bit') / hours('Bas','Bit')", description: 'Tarih ve saat farkı.' },
|
{
|
||||||
{ signature: "tpl('{Alan} - {value}')", description: 'Token doldurur.' },
|
signature: "selected('Yol') / snum() / sstr()",
|
||||||
{ signature: "notify('mesaj') / ask('mesaj')", description: 'Uyarı ve onay. ask false ise geri alır.' },
|
description: 'ListForms.ScriptBuilder.Api.Selected',
|
||||||
{ signature: "openUrl('/adres')", description: 'Yeni sekmede açar.' },
|
},
|
||||||
{ signature: "await api('/api/x', 'data.name')", description: 'JSON çağrısı yapar.' },
|
{ signature: "readOnly('Alan', kosul)", description: 'ListForms.ScriptBuilder.Api.ReadOnly' },
|
||||||
|
{ signature: "round(x, 2) / sum('A','B')", description: 'ListForms.ScriptBuilder.Api.Math' },
|
||||||
|
{
|
||||||
|
signature: "days('Bas','Bit') / hours('Bas','Bit')",
|
||||||
|
description: 'ListForms.ScriptBuilder.Api.DateDiff',
|
||||||
|
},
|
||||||
|
{ signature: "tpl('{Alan} - {value}')", description: 'ListForms.ScriptBuilder.Api.Tpl' },
|
||||||
|
{
|
||||||
|
signature: "notify('mesaj') / ask('mesaj')",
|
||||||
|
description: 'ListForms.ScriptBuilder.Api.NotifyAsk',
|
||||||
|
},
|
||||||
|
{ signature: "openUrl('/adres')", description: 'ListForms.ScriptBuilder.Api.OpenUrl' },
|
||||||
|
{ signature: "await api('/api/x', 'data.name')", description: 'ListForms.ScriptBuilder.Api.Api' },
|
||||||
{
|
{
|
||||||
signature: 'isReady',
|
signature: 'isReady',
|
||||||
description:
|
description: 'ListForms.ScriptBuilder.Api.IsReady',
|
||||||
'Form açılışında true, değer değişiminde false. Script açılışta da çalışsın istiyorsan bu ifadeyi kullan.',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -618,7 +618,7 @@ const Wizard = () => {
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard file.'}
|
{translate('::App.Listforms.WizardFileLoadError')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -789,7 +789,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
const handleDeploy = async () => {
|
const handleDeploy = async () => {
|
||||||
const formik = formikRef.current
|
const formik = formikRef.current
|
||||||
if (!formik) throw new Error(translate('::App.Platform.Unknown') || 'Form bulunamadı')
|
if (!formik) throw new Error(translate('::App.Platform.Unknown'))
|
||||||
|
|
||||||
const values = formik.values
|
const values = formik.values
|
||||||
|
|
||||||
|
|
@ -861,11 +861,13 @@ const Wizard = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageTitle title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))} />
|
<PageTitle
|
||||||
|
title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))}
|
||||||
|
/>
|
||||||
|
|
||||||
{isLoadingEditData && (
|
{isLoadingEditData && (
|
||||||
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">
|
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">
|
||||||
{translate('::App.Loading') || 'Loading...'}
|
{translate('::App.Loading')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -879,32 +881,32 @@ const Wizard = () => {
|
||||||
>
|
>
|
||||||
{({ touched, errors, values }) => {
|
{({ touched, errors, values }) => {
|
||||||
const activeSteps = [
|
const activeSteps = [
|
||||||
{ id: 0, title: translate('::ListForms.Wizard.MenuInfo') || 'Menu Info' },
|
{ id: 0, title: translate('::ListForms.Wizard.MenuInfo') },
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings',
|
title: translate('::ListForms.Wizard.ListFormSettings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields',
|
title: translate('::ListForms.Wizard.ListFormFields'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
|
title: translate('::ListForms.ListFormEdit.SubForms'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
|
title: translate('::ListForms.ListFormEdit.TabWidgets'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
|
title: translate('::ListForms.ListFormEdit.TabWorkflow'),
|
||||||
},
|
},
|
||||||
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
|
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
title: translate(step.translationKey) || step.fallbackTitle,
|
title: translate(step.translationKey) || step.fallbackTitle,
|
||||||
})),
|
})),
|
||||||
{ id: 10, title: translate('::App.Platform.Deploy') || 'Deploy' },
|
{ id: 10, title: translate('::App.Platform.Deploy') },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa
|
// Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa
|
||||||
|
|
@ -931,7 +933,7 @@ const Wizard = () => {
|
||||||
<Steps.Item
|
<Steps.Item
|
||||||
customIcon={<FaFolderOpen />}
|
customIcon={<FaFolderOpen />}
|
||||||
status="pending"
|
status="pending"
|
||||||
title={translate('::App.Listforms.WizardManager') || 'Wizard Manager'}
|
title={translate('::App.Listforms.WizardManager')}
|
||||||
onStepChange={() =>
|
onStepChange={() =>
|
||||||
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager)
|
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ const WizardFileManager = () => {
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard files.'}
|
{translate('::App.Listforms.WizardFileLoadError')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -79,8 +79,7 @@ const WizardFileManager = () => {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={3000}>
|
<Notification type="success" duration={3000}>
|
||||||
<strong>{confirm.wizardName}</strong>{' '}
|
<strong>{confirm.wizardName}</strong>{' '}
|
||||||
{translate('::App.Listforms.WizardFileDeleteSuccess') ||
|
{translate('::App.Listforms.WizardFileDeleteSuccess')}
|
||||||
'wizard file deleted successfully.'}
|
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -88,8 +87,7 @@ const WizardFileManager = () => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
{translate('::App.Listforms.WizardFileDeleteError') || 'Failed to delete wizard file.'}:{' '}
|
{translate('::App.Listforms.WizardFileDeleteError')}: {err?.message ?? 'Unknown error'}
|
||||||
{err?.message ?? 'Unknown error'}
|
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -110,7 +108,7 @@ const WizardFileManager = () => {
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
{MenuIcon}
|
{MenuIcon}
|
||||||
<h4 className="text-sm font-medium truncate">
|
<h4 className="text-sm font-medium truncate">
|
||||||
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
|
{translate('::App.Listforms.WizardManager')}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -120,7 +118,7 @@ const WizardFileManager = () => {
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="pl-6 w-36 sm:w-44"
|
className="pl-6 w-36 sm:w-44"
|
||||||
placeholder={translate('::App.Platform.Search') || 'Search...'}
|
placeholder={translate('::App.Platform.Search')}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -128,7 +126,7 @@ const WizardFileManager = () => {
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
title={translate('::App.Platform.Refresh') || 'Yenile'}
|
title={translate('::App.Platform.Refresh')}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={loadFiles}
|
onClick={loadFiles}
|
||||||
>
|
>
|
||||||
|
|
@ -142,7 +140,7 @@ const WizardFileManager = () => {
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
>
|
>
|
||||||
<FaPlus className="mr-1" />
|
<FaPlus className="mr-1" />
|
||||||
<span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}</span>
|
<span className="hidden sm:inline">{translate('::ListForms.Wizard.AddNewRecord')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -152,7 +150,7 @@ const WizardFileManager = () => {
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||||
<p className="text-sm text-gray-400 text-center py-4">
|
<p className="text-sm text-gray-400 text-center py-4">
|
||||||
{translate('::App.Listforms.WizardNoFiles') || 'No wizard files found.'}
|
{translate('::App.Listforms.WizardNoFiles')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -188,10 +186,7 @@ const WizardFileManager = () => {
|
||||||
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
||||||
{!f.hasInsertedRecords && (
|
{!f.hasInsertedRecords && (
|
||||||
<span
|
<span
|
||||||
title={
|
title={translate('::App.Listforms.WizardFileNoTrackedRecords')}
|
||||||
translate('::App.Listforms.WizardFileNoTrackedRecords') ||
|
|
||||||
'Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir.'
|
|
||||||
}
|
|
||||||
className="text-yellow-500 text-xs flex items-center gap-1"
|
className="text-yellow-500 text-xs flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<FaExclamationTriangle />
|
<FaExclamationTriangle />
|
||||||
|
|
@ -202,7 +197,7 @@ const WizardFileManager = () => {
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
|
className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
|
||||||
type="button"
|
type="button"
|
||||||
title={translate('::App.Platform.Edit') || 'Edit'}
|
title={translate('::App.Platform.Edit')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
|
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
|
||||||
state: { editFileName: f.fileName },
|
state: { editFileName: f.fileName },
|
||||||
|
|
@ -216,7 +211,7 @@ const WizardFileManager = () => {
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
type="button"
|
type="button"
|
||||||
title={translate('::Delete') || 'Delete'}
|
title={translate('::Delete')}
|
||||||
loading={deletingFile === f.fileName}
|
loading={deletingFile === f.fileName}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
|
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
|
||||||
|
|
@ -256,7 +251,7 @@ const WizardFileManager = () => {
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}>
|
<Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}>
|
||||||
{translate('::Delete') || 'Yes, Delete'}
|
{translate('::Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ function MenuTreeInline({
|
||||||
if (!node.id) return
|
if (!node.id) return
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
`"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm') || 'menüsünü silmek istediğinize emin misiniz?'}`,
|
`"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm')}`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -422,7 +422,7 @@ function MenuTreeInline({
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||||
) : enrichedNodes.length === 0 ? (
|
) : enrichedNodes.length === 0 ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">
|
<div className="px-4 py-3 text-sm text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.NoMenusAvailable') || 'No menus available'}
|
{translate('::ListForms.Wizard.Step1.NoMenusAvailable')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
enrichedNodes.map((node) => (
|
enrichedNodes.map((node) => (
|
||||||
|
|
@ -504,8 +504,7 @@ const WizardStep1 = ({
|
||||||
!values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
|
!values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
|
||||||
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
|
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
|
||||||
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
|
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
|
||||||
!(Number(values.menuOrder) >= 1) &&
|
!(Number(values.menuOrder) >= 1) && translate('::ListForms.Wizard.Step1.MenuOrder'),
|
||||||
(translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'),
|
|
||||||
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
|
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
|
||||||
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
|
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
|
||||||
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
|
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
|
||||||
|
|
@ -521,8 +520,7 @@ const WizardStep1 = ({
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={
|
extra={
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.WizardNameHint') ||
|
{translate('::ListForms.Wizard.Step1.WizardNameHint')}
|
||||||
'Used to generate ListForm Code and Menu Code'}
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -561,9 +559,7 @@ const WizardStep1 = ({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
icon={<FaPlus className="text-xs" />}
|
icon={<FaPlus className="text-xs" />}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">{translate('::ListForms.Wizard.Add')}</span>
|
||||||
{translate('::ListForms.Wizard.Add') || 'Ekle'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
{values.menuParentCode && (
|
{values.menuParentCode && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -578,7 +574,7 @@ const WizardStep1 = ({
|
||||||
icon={<FaTimes className="text-xs" />}
|
icon={<FaTimes className="text-xs" />}
|
||||||
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
|
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'}
|
{translate('::ListForms.Wizard.ClearSelection')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -624,7 +620,7 @@ const WizardStep1 = ({
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={
|
extra={
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.MenuCodeHint') || 'Auto-derived, editable'}
|
{translate('::ListForms.Wizard.Step1.MenuCodeHint')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -640,14 +636,13 @@ const WizardStep1 = ({
|
||||||
|
|
||||||
{/* Menu Order */}
|
{/* Menu Order */}
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'}
|
label={translate('::ListForms.Wizard.Step1.MenuOrder')}
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
invalid={!!(errors.menuOrder && touched.menuOrder)}
|
invalid={!!(errors.menuOrder && touched.menuOrder)}
|
||||||
errorMessage={errors.menuOrder}
|
errorMessage={errors.menuOrder}
|
||||||
extra={
|
extra={
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.MenuOrderHint') ||
|
{translate('::ListForms.Wizard.Step1.MenuOrderHint')}
|
||||||
'Automatically assigned, editable'}
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -690,9 +685,7 @@ const WizardStep1 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextMenuEn"
|
name="languageTextMenuEn"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameEnglish')}
|
||||||
translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -708,9 +701,7 @@ const WizardStep1 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextMenuTr"
|
name="languageTextMenuTr"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameTurkish')}
|
||||||
translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -756,7 +747,7 @@ const WizardStep1 = ({
|
||||||
<div className="flex-1 flex items-center gap-2">
|
<div className="flex-1 flex items-center gap-2">
|
||||||
{!step1CanGo && (
|
{!step1CanGo && (
|
||||||
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
|
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
|
||||||
⚠ {translate('::ListForms.Wizard.Required') || 'Zorunlu'}: {step1Missing.join(', ')}
|
⚠ {translate('::ListForms.Wizard.Required')}: {step1Missing.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -767,7 +758,7 @@ const WizardStep1 = ({
|
||||||
disabled={!step1CanGo}
|
disabled={!step1CanGo}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -188,10 +188,7 @@ const WizardStep11 = ({
|
||||||
const [isDone, setIsDone] = useState(false)
|
const [isDone, setIsDone] = useState(false)
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
const steps = useMemo(
|
const steps = useMemo(() => buildLogSteps(values, groups, translate), [values, groups, translate])
|
||||||
() => buildLogSteps(values, groups, translate),
|
|
||||||
[values, groups, translate],
|
|
||||||
)
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
|
||||||
|
|
@ -344,10 +341,8 @@ const WizardStep11 = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
title={
|
title={translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout')}
|
||||||
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
|
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm')}`}
|
||||||
}
|
|
||||||
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
|
|
||||||
>
|
>
|
||||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
|
|
@ -357,7 +352,7 @@ const WizardStep11 = ({
|
||||||
className: 'text-indigo-600 dark:text-indigo-400',
|
className: 'text-indigo-600 dark:text-indigo-400',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
|
label: translate('::ListForms.Wizard.Step4.EditingForm'),
|
||||||
value: editingFormFields.length,
|
value: editingFormFields.length,
|
||||||
className: 'text-emerald-600 dark:text-emerald-400',
|
className: 'text-emerald-600 dark:text-emerald-400',
|
||||||
},
|
},
|
||||||
|
|
@ -401,7 +396,7 @@ const WizardStep11 = ({
|
||||||
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
{g.items.length === 0 ? (
|
{g.items.length === 0 ? (
|
||||||
<span className="block px-3 py-2 text-xs text-gray-300 italic">
|
<span className="block px-3 py-2 text-xs text-gray-300 italic">
|
||||||
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
|
{translate('::ListForms.Wizard.Step4.NoFields')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
g.items.map((item) => {
|
g.items.map((item) => {
|
||||||
|
|
@ -425,11 +420,11 @@ const WizardStep11 = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap justify-end gap-1">
|
<div className="flex flex-wrap justify-end gap-1">
|
||||||
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
|
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
|
||||||
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
|
{translate('::ListForms.Wizard.Step4.SelectedColumns')}
|
||||||
</span>
|
</span>
|
||||||
{isPopupField && (
|
{isPopupField && (
|
||||||
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
|
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
|
||||||
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
|
{translate('::ListForms.Wizard.Step4.EditingForm')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isKeyField && (
|
{isKeyField && (
|
||||||
|
|
@ -455,7 +450,7 @@ const WizardStep11 = ({
|
||||||
{ungroupedSelectedColumns.length > 0 && (
|
{ungroupedSelectedColumns.length > 0 && (
|
||||||
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
|
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
|
||||||
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||||
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
|
{translate('::ListForms.Wizard.Step4.UngroupedColumns')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{ungroupedSelectedColumns.map((col) => {
|
{ungroupedSelectedColumns.map((col) => {
|
||||||
|
|
@ -481,10 +476,7 @@ const WizardStep11 = ({
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{subForms.length > 0 && (
|
{subForms.length > 0 && (
|
||||||
<Section
|
<Section title={translate('::ListForms.ListFormEdit.SubForms')} badge={subForms.length}>
|
||||||
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
|
|
||||||
badge={subForms.length}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{subForms.map((subForm, index) => (
|
{subForms.map((subForm, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -517,10 +509,7 @@ const WizardStep11 = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgets.length > 0 && (
|
{widgets.length > 0 && (
|
||||||
<Section
|
<Section title={translate('::ListForms.ListFormEdit.TabWidgets')} badge={widgets.length}>
|
||||||
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
|
|
||||||
badge={widgets.length}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{widgets.map((widget, index) => (
|
{widgets.map((widget, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -545,7 +534,7 @@ const WizardStep11 = ({
|
||||||
|
|
||||||
{(hasWorkflowFields || workflowItems.length > 0) && (
|
{(hasWorkflowFields || workflowItems.length > 0) && (
|
||||||
<Section
|
<Section
|
||||||
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'}
|
title={translate('::ListForms.ListFormEdit.TabWorkflow')}
|
||||||
badge={workflowItems.length}
|
badge={workflowItems.length}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
|
@ -594,7 +583,7 @@ const WizardStep11 = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{values.todo && (
|
{values.todo && (
|
||||||
<Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}>
|
<Section title={translate('::ListForms.ListFormEdit.TabTodo')}>
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
<Row
|
<Row
|
||||||
label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')}
|
label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')}
|
||||||
|
|
@ -657,7 +646,7 @@ const WizardStep11 = ({
|
||||||
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
|
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
|
||||||
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
|
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
|
label: translate('::ListForms.Wizard.Step4.EditingForm'),
|
||||||
value: editingFormFields.length,
|
value: editingFormFields.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -665,19 +654,19 @@ const WizardStep11 = ({
|
||||||
value: selectedColumns.size,
|
value: selectedColumns.size,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
|
label: translate('::ListForms.ListFormEdit.SubForms'),
|
||||||
value: subForms.length,
|
value: subForms.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
|
label: translate('::ListForms.ListFormEdit.TabWidgets'),
|
||||||
value: widgets.length,
|
value: widgets.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
|
label: translate('::ListForms.ListFormEdit.TabWorkflow'),
|
||||||
value: workflowItems.length,
|
value: workflowItems.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo',
|
label: translate('::ListForms.ListFormEdit.TabTodo'),
|
||||||
value: values.todo ? 1 : 0,
|
value: values.todo ? 1 : 0,
|
||||||
},
|
},
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
|
|
@ -698,7 +687,7 @@ const WizardStep11 = ({
|
||||||
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
|
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
|
||||||
<FaRocket className="text-indigo-400 text-xs" />
|
<FaRocket className="text-indigo-400 text-xs" />
|
||||||
{translate('::ListForms.Wizard.Step4.DeployLog') || 'Deploy Log'}
|
{translate('::ListForms.Wizard.Step4.DeployLog')}
|
||||||
</span>
|
</span>
|
||||||
{isDone && (
|
{isDone && (
|
||||||
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
|
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
|
||||||
|
|
@ -771,7 +760,7 @@ const WizardStep11 = ({
|
||||||
disabled={isDeploying}
|
disabled={isDeploying}
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
>
|
>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end">
|
<div className="flex-1 flex items-center justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,7 @@ const WizardStep2 = ({
|
||||||
!values.languageTextDescTr && translate('::ListForms.Wizard.Step2.DescriptionTextTurkish'),
|
!values.languageTextDescTr && translate('::ListForms.Wizard.Step2.DescriptionTextTurkish'),
|
||||||
!values.languageTextTitleEn && translate('::ListForms.Wizard.Step2.TitleTextEnglish'),
|
!values.languageTextTitleEn && translate('::ListForms.Wizard.Step2.TitleTextEnglish'),
|
||||||
!values.languageTextTitleTr && translate('::ListForms.Wizard.Step2.TitleTextTurkish'),
|
!values.languageTextTitleTr && translate('::ListForms.Wizard.Step2.TitleTextTurkish'),
|
||||||
selectedColumns.size === 0 &&
|
selectedColumns.size === 0 && translate('::ListForms.Wizard.Step2.ColumnSelection'),
|
||||||
(translate('::ListForms.Wizard.Step2.ColumnSelection') || 'Sütun seçimi'),
|
|
||||||
].filter(Boolean) as string[]
|
].filter(Boolean) as string[]
|
||||||
const step2CanGo = step2Missing.length === 0
|
const step2CanGo = step2Missing.length === 0
|
||||||
|
|
||||||
|
|
@ -140,8 +139,7 @@ const WizardStep2 = ({
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={
|
extra={
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step2.ListFormCodeHint') ||
|
{translate('::ListForms.Wizard.Step2.ListFormCodeHint')}
|
||||||
'Auto-derived from Wizard Name, editable'}
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -230,7 +228,7 @@ const WizardStep2 = ({
|
||||||
const grouped = dbObjects
|
const grouped = dbObjects
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.Wizard.Step2.Tables') || 'Tables',
|
label: translate('::ListForms.Wizard.Step2.Tables'),
|
||||||
options: dbObjects.tables.map((t) => ({
|
options: dbObjects.tables.map((t) => ({
|
||||||
label: t.tableName,
|
label: t.tableName,
|
||||||
value: t.tableName,
|
value: t.tableName,
|
||||||
|
|
@ -240,9 +238,7 @@ const WizardStep2 = ({
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:
|
label: translate('::ListForms.Wizard.Step2.StoredProcedures'),
|
||||||
translate('::ListForms.Wizard.Step2.StoredProcedures') ||
|
|
||||||
'Stored Procedures',
|
|
||||||
options: dbObjects.storedProcedures.map((p) => ({
|
options: dbObjects.storedProcedures.map((p) => ({
|
||||||
label: p.objectName,
|
label: p.objectName,
|
||||||
value: p.objectName,
|
value: p.objectName,
|
||||||
|
|
@ -252,7 +248,7 @@ const WizardStep2 = ({
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::App.Platform.Views') || 'Views',
|
label: translate('::App.Platform.Views'),
|
||||||
options: dbObjects.views.map((v) => ({
|
options: dbObjects.views.map((v) => ({
|
||||||
label: v.objectName,
|
label: v.objectName,
|
||||||
value: v.objectName,
|
value: v.objectName,
|
||||||
|
|
@ -262,7 +258,7 @@ const WizardStep2 = ({
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('::ListForms.Wizard.Step2.Functions') || 'Functions',
|
label: translate('::ListForms.Wizard.Step2.Functions'),
|
||||||
options: dbObjects.functions.map((f) => ({
|
options: dbObjects.functions.map((f) => ({
|
||||||
label: f.objectName,
|
label: f.objectName,
|
||||||
value: f.objectName,
|
value: f.objectName,
|
||||||
|
|
@ -285,8 +281,7 @@ const WizardStep2 = ({
|
||||||
placeholder={
|
placeholder={
|
||||||
isLoadingDbObjects
|
isLoadingDbObjects
|
||||||
? translate('::App.Loading')
|
? translate('::App.Loading')
|
||||||
: translate('::ListForms.Wizard.Step2.SelectCommand') ||
|
: translate('::ListForms.Wizard.Step2.SelectCommand')
|
||||||
'Tablo/View/SP seç veya SQL yaz…'
|
|
||||||
}
|
}
|
||||||
value={field.value ? { label: field.value, value: field.value } : null}
|
value={field.value ? { label: field.value, value: field.value } : null}
|
||||||
onChange={(option: any) => {
|
onChange={(option: any) => {
|
||||||
|
|
@ -369,8 +364,8 @@ const WizardStep2 = ({
|
||||||
isLoading={isLoadingColumns}
|
isLoading={isLoadingColumns}
|
||||||
placeholder={
|
placeholder={
|
||||||
isLoadingColumns
|
isLoadingColumns
|
||||||
? translate('::ListForms.Wizard.Step2.ColumnsLoading') || 'Sütunlar yükleniyor…'
|
? translate('::ListForms.Wizard.Step2.ColumnsLoading')
|
||||||
: translate('::ListForms.Wizard.Step2.SelectKeyColumn') || 'Key sütunu seç…'
|
: translate('::ListForms.Wizard.Step2.SelectKeyColumn')
|
||||||
}
|
}
|
||||||
options={selectCommandColumns.map((c) => ({
|
options={selectCommandColumns.map((c) => ({
|
||||||
label: `${c.columnName} (${c.dataType})`,
|
label: `${c.columnName} (${c.dataType})`,
|
||||||
|
|
@ -639,9 +634,7 @@ const WizardStep2 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextTitleEn"
|
name="languageTextTitleEn"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step2.TitleTextEnglish')}
|
||||||
translate('::ListForms.Wizard.Step2.TitleTextEnglish') || 'English Title Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -656,9 +649,7 @@ const WizardStep2 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextTitleTr"
|
name="languageTextTitleTr"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step2.TitleTextTurkish')}
|
||||||
translate('::ListForms.Wizard.Step2.TitleTextTurkish') || 'Turkish Title Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -673,10 +664,7 @@ const WizardStep2 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextDescEn"
|
name="languageTextDescEn"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step2.DescriptionTextEnglish')}
|
||||||
translate('::ListForms.Wizard.Step2.DescriptionTextEnglish') ||
|
|
||||||
'English Description Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -691,10 +679,7 @@ const WizardStep2 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextDescTr"
|
name="languageTextDescTr"
|
||||||
placeholder={
|
placeholder={translate('::ListForms.Wizard.Step2.DescriptionTextTurkish')}
|
||||||
translate('::ListForms.Wizard.Step2.DescriptionTextTurkish') ||
|
|
||||||
'Turkish Description Text'
|
|
||||||
}
|
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -702,7 +687,7 @@ const WizardStep2 = ({
|
||||||
|
|
||||||
{/* Column Selection Panel */}
|
{/* Column Selection Panel */}
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::ListForms.Wizard.Step2.Columns') || 'Sütunlar'}
|
label={translate('::ListForms.Wizard.Step2.Columns')}
|
||||||
extra={
|
extra={
|
||||||
selectCommandColumns.length > 0 ? (
|
selectCommandColumns.length > 0 ? (
|
||||||
<div className="flex items-center gap-2 ml-3">
|
<div className="flex items-center gap-2 ml-3">
|
||||||
|
|
@ -712,7 +697,7 @@ const WizardStep2 = ({
|
||||||
onClick={() => onToggleAllColumns(true)}
|
onClick={() => onToggleAllColumns(true)}
|
||||||
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
|
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step2.SelectAll') || 'Tümünü Seç'}
|
{translate('::ListForms.Wizard.Step2.SelectAll')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|
@ -720,7 +705,7 @@ const WizardStep2 = ({
|
||||||
onClick={() => onToggleAllColumns(false)}
|
onClick={() => onToggleAllColumns(false)}
|
||||||
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step2.ClearAll') || 'Tümünü Kaldır'}
|
{translate('::ListForms.Wizard.Step2.ClearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{selectedColumns.size}/{selectCommandColumns.length}{' '}
|
{selectedColumns.size}/{selectCommandColumns.length}{' '}
|
||||||
|
|
@ -733,12 +718,11 @@ const WizardStep2 = ({
|
||||||
<div className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 overflow-hidden">
|
<div className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 overflow-hidden">
|
||||||
{isLoadingColumns ? (
|
{isLoadingColumns ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">
|
<div className="px-4 py-3 text-sm text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step2.ColumnsLoading') || 'Sütunlar yükleniyor…'}
|
{translate('::ListForms.Wizard.Step2.ColumnsLoading')}
|
||||||
</div>
|
</div>
|
||||||
) : selectCommandColumns.length === 0 ? (
|
) : selectCommandColumns.length === 0 ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">
|
<div className="px-4 py-3 text-sm text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step2.ColumnsWillAppear') ||
|
{translate('::ListForms.Wizard.Step2.ColumnsWillAppear')}
|
||||||
'Select Command seçilince sütunlar burada görünecek'}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-40 overflow-y-auto py-1">
|
<div className="h-40 overflow-y-auto py-1">
|
||||||
|
|
@ -775,7 +759,7 @@ const WizardStep2 = ({
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
|
||||||
<div className="flex items-center gap-3 w-full">
|
<div className="flex items-center gap-3 w-full">
|
||||||
<Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
<Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end gap-3">
|
<div className="flex-1 flex items-center justify-end gap-3">
|
||||||
{!step2CanGo && (
|
{!step2CanGo && (
|
||||||
|
|
@ -790,7 +774,7 @@ const WizardStep2 = ({
|
||||||
disabled={!step2CanGo}
|
disabled={!step2CanGo}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@ function SortableItem({
|
||||||
icon={<FaPlus className="text-[8px]" />}
|
icon={<FaPlus className="text-[8px]" />}
|
||||||
className="!h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40 shrink-0"
|
className="!h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40 shrink-0"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
{translate('::ListForms.Wizard.Step3.GenerateFromTable')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -423,7 +423,7 @@ function SortableItem({
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||||
{pickerStep === 'table'
|
{pickerStep === 'table'
|
||||||
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
|
? translate('::ListForms.Wizard.Step3.SelectTable')
|
||||||
: (pickerTable?.tableName ?? '')}
|
: (pickerTable?.tableName ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -445,15 +445,14 @@ function SortableItem({
|
||||||
autoFocus
|
autoFocus
|
||||||
value={tableSearch}
|
value={tableSearch}
|
||||||
onChange={(e) => setTableSearch(e.target.value)}
|
onChange={(e) => setTableSearch(e.target.value)}
|
||||||
placeholder={translate('::Search') || 'Ara...'}
|
placeholder={translate('::Search')}
|
||||||
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto flex-1 p-2">
|
<div className="overflow-y-auto flex-1 p-2">
|
||||||
{!dbObjects ? (
|
{!dbObjects ? (
|
||||||
<div className="text-xs text-gray-400 text-center py-6">
|
<div className="text-xs text-gray-400 text-center py-6">
|
||||||
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') ||
|
{translate('::ListForms.Wizard.Step3.NoTablesAvailable')}
|
||||||
'Tablo bulunamadı'}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
dbObjects.tables
|
dbObjects.tables
|
||||||
|
|
@ -506,14 +505,14 @@ function SortableItem({
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
||||||
Key Sütunu
|
{translate('::ListForms.ListFormFieldEdit.KeyColumn')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={pickerKeyCol}
|
value={pickerKeyCol}
|
||||||
onChange={(e) => setPickerKeyCol(e.target.value)}
|
onChange={(e) => setPickerKeyCol(e.target.value)}
|
||||||
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
>
|
>
|
||||||
<option value="">-- Seçiniz --</option>
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{pickerColumns.map((c) => (
|
{pickerColumns.map((c) => (
|
||||||
<option key={c.columnName} value={c.columnName}>
|
<option key={c.columnName} value={c.columnName}>
|
||||||
{c.columnName}
|
{c.columnName}
|
||||||
|
|
@ -523,14 +522,14 @@ function SortableItem({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
||||||
Name Sütunu
|
{translate('::ListForms.ListFormFieldEdit.NameColumn')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={pickerNameCol}
|
value={pickerNameCol}
|
||||||
onChange={(e) => setPickerNameCol(e.target.value)}
|
onChange={(e) => setPickerNameCol(e.target.value)}
|
||||||
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
|
||||||
>
|
>
|
||||||
<option value="">-- Seçiniz --</option>
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{pickerColumns.map((c) => (
|
{pickerColumns.map((c) => (
|
||||||
<option key={c.columnName} value={c.columnName}>
|
<option key={c.columnName} value={c.columnName}>
|
||||||
{c.columnName}
|
{c.columnName}
|
||||||
|
|
@ -565,7 +564,7 @@ function SortableItem({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40"
|
className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Tamam
|
{translate('::OK')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -659,7 +658,7 @@ function SortableItem({
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label
|
||||||
className="flex items-center gap-1 cursor-pointer ml-auto"
|
className="flex items-center gap-1 cursor-pointer ml-auto"
|
||||||
title={translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'}
|
title={translate('::ListForms.Wizard.Step3.IncludeInEditingForm')}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -668,7 +667,7 @@ function SortableItem({
|
||||||
className="w-3 h-3 accent-indigo-500"
|
className="w-3 h-3 accent-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-gray-400">
|
<span className="text-[10px] text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'}
|
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm')}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-1 cursor-pointer ml-auto" title="Required">
|
<label className="flex items-center gap-1 cursor-pointer ml-auto" title="Required">
|
||||||
|
|
@ -679,7 +678,7 @@ function SortableItem({
|
||||||
className="w-3 h-3 accent-red-500"
|
className="w-3 h-3 accent-red-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-gray-400">
|
<span className="text-[10px] text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step3.Required') || 'Required'}
|
{translate('::ListForms.Wizard.Step3.Required')}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -741,7 +740,7 @@ function GroupCard({
|
||||||
{/* ColCount */}
|
{/* ColCount */}
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
|
{translate('::ListForms.Wizard.Step3.Cols')}
|
||||||
</span>
|
</span>
|
||||||
{[1, 2, 3].map((n) => (
|
{[1, 2, 3].map((n) => (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -769,12 +768,9 @@ function GroupCard({
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaArrowRight className="text-[9px]" />}
|
icon={<FaArrowRight className="text-[9px]" />}
|
||||||
className="!h-6 !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-2 text-[11px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-900/40 shrink-0"
|
className="!h-6 !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-2 text-[11px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-900/40 shrink-0"
|
||||||
title={
|
title={translate('::ListForms.Wizard.Step3.AddAllToGroupTitle')}
|
||||||
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
|
|
||||||
'Tüm mevcut sütunları bu gruba ekle'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
|
{translate('::ListForms.Wizard.Step3.AddAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -784,7 +780,7 @@ function GroupCard({
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={<FaTrash className="text-xs" />}
|
icon={<FaTrash className="text-xs" />}
|
||||||
className="!h-7 !w-7 !px-0 text-gray-300 transition-colors hover:!bg-transparent hover:text-red-500"
|
className="!h-7 !w-7 !px-0 text-gray-300 transition-colors hover:!bg-transparent hover:text-red-500"
|
||||||
title={translate('::ListForms.Wizard.Step3.DeleteGroup') || 'Delete group'}
|
title={translate('::ListForms.Wizard.Step3.DeleteGroup')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -800,7 +796,7 @@ function GroupCard({
|
||||||
>
|
>
|
||||||
{group.items.length === 0 && !isOver && (
|
{group.items.length === 0 && !isOver && (
|
||||||
<div className="flex items-center justify-center h-12 text-xs text-gray-300 dark:text-gray-600 select-none">
|
<div className="flex items-center justify-center h-12 text-xs text-gray-300 dark:text-gray-600 select-none">
|
||||||
{translate('::ListForms.Wizard.Step3.DragColumnsHere') || 'Sütunları buraya sürükleyin'}
|
{translate('::ListForms.Wizard.Step3.DragColumnsHere')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<SortableContext items={itemIds} strategy={rectSortingStrategy}>
|
<SortableContext items={itemIds} strategy={rectSortingStrategy}>
|
||||||
|
|
@ -1085,10 +1081,9 @@ const WizardStep3 = ({
|
||||||
const hasEmptyGroup = groups.some((g) => g.items.length === 0)
|
const hasEmptyGroup = groups.some((g) => g.items.length === 0)
|
||||||
const canProceed = !hasNoGroups && !hasEmptyGroup
|
const canProceed = !hasNoGroups && !hasEmptyGroup
|
||||||
const validationMsg = hasNoGroups
|
const validationMsg = hasNoGroups
|
||||||
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.'
|
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup')
|
||||||
: hasEmptyGroup
|
: hasEmptyGroup
|
||||||
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn') ||
|
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn')
|
||||||
'Her gruba en az bir sütun eklemelisiniz.'
|
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1114,8 +1109,7 @@ const WizardStep3 = ({
|
||||||
<div className="flex flex-row flex-wrap lg:flex-col gap-1.5 max-h-40 lg:max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
|
<div className="flex flex-row flex-wrap lg:flex-col gap-1.5 max-h-40 lg:max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
|
||||||
{availableColumns.length === 0 ? (
|
{availableColumns.length === 0 ? (
|
||||||
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full">
|
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full">
|
||||||
{translate('::ListForms.Wizard.Step3.AllColumnsAdded') ||
|
{translate('::ListForms.Wizard.Step3.AllColumnsAdded')}
|
||||||
'Tüm sütunlar gruplara eklendi'}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
|
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
|
||||||
|
|
@ -1128,8 +1122,7 @@ const WizardStep3 = ({
|
||||||
<div className="flex-1 flex flex-col gap-3">
|
<div className="flex-1 flex flex-col gap-3">
|
||||||
{groups.length === 0 && (
|
{groups.length === 0 && (
|
||||||
<div className="rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 flex items-center justify-center h-36 text-sm text-gray-300 dark:text-gray-600 select-none">
|
<div className="rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 flex items-center justify-center h-36 text-sm text-gray-300 dark:text-gray-600 select-none">
|
||||||
{translate('::ListForms.Wizard.Step3.NoGroupsYet') ||
|
{translate('::ListForms.Wizard.Step3.NoGroupsYet')}
|
||||||
'Henüz grup yok — aşağıdan grup ekleyin'}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1160,7 +1153,7 @@ const WizardStep3 = ({
|
||||||
icon={<FaPlus className="text-xs" />}
|
icon={<FaPlus className="text-xs" />}
|
||||||
className="w-full !h-auto !items-center !justify-center gap-2 !rounded-xl border-2 border-dashed border-gray-200 !py-2.5 text-sm text-gray-400 transition-colors hover:!bg-transparent hover:border-indigo-400 hover:text-indigo-500 dark:border-gray-700 dark:text-gray-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400"
|
className="w-full !h-auto !items-center !justify-center gap-2 !rounded-xl border-2 border-dashed border-gray-200 !py-2.5 text-sm text-gray-400 transition-colors hover:!bg-transparent hover:border-indigo-400 hover:text-indigo-500 dark:border-gray-700 dark:text-gray-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Step3.AddGroup') || 'Grup Ekle'}
|
{translate('::ListForms.Wizard.Step3.AddGroup')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1239,7 +1232,7 @@ const WizardStep3 = ({
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
||||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||||
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1248,7 +1241,7 @@ const WizardStep3 = ({
|
||||||
icon={<FaCode />}
|
icon={<FaCode />}
|
||||||
onClick={() => setIsHelperOpen(true)}
|
onClick={() => setIsHelperOpen(true)}
|
||||||
>
|
>
|
||||||
{translate('::Helper Codes') || 'Helper Codes'}
|
{translate('::Helper Codes')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end gap-3">
|
<div className="flex-1 flex items-center justify-end gap-3">
|
||||||
{!canProceed && (
|
{!canProceed && (
|
||||||
|
|
@ -1264,7 +1257,7 @@ const WizardStep3 = ({
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ function WizardStep4({
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
||||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||||
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end gap-3">
|
<div className="flex-1 flex items-center justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -229,7 +229,7 @@ function WizardStep4({
|
||||||
icon={<FaArrowRight />}
|
icon={<FaArrowRight />}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
||||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||||
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end gap-3">
|
<div className="flex-1 flex items-center justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -153,7 +153,7 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
|
||||||
icon={<FaArrowRight />}
|
icon={<FaArrowRight />}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ function WizardStep6({
|
||||||
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
|
||||||
id: startId,
|
id: startId,
|
||||||
nodeId: startId,
|
nodeId: startId,
|
||||||
title: 'İş Akışı Başlat1',
|
title: translate('::ListForms.Workflow.Demo.Start'),
|
||||||
nextOnStart: approval1Id,
|
nextOnStart: approval1Id,
|
||||||
positionX: 34,
|
positionX: 34,
|
||||||
positionY: 28,
|
positionY: 28,
|
||||||
|
|
@ -232,7 +232,7 @@ function WizardStep6({
|
||||||
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
||||||
id: approval1Id,
|
id: approval1Id,
|
||||||
nodeId: approval1Id,
|
nodeId: approval1Id,
|
||||||
title: 'Onay1',
|
title: translate('::ListForms.Workflow.Demo.Approval1'),
|
||||||
nextOnApprove: approval2Id,
|
nextOnApprove: approval2Id,
|
||||||
nextOnReject: informId,
|
nextOnReject: informId,
|
||||||
positionX: 323,
|
positionX: 323,
|
||||||
|
|
@ -242,7 +242,7 @@ function WizardStep6({
|
||||||
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
||||||
id: approval2Id,
|
id: approval2Id,
|
||||||
nodeId: approval2Id,
|
nodeId: approval2Id,
|
||||||
title: 'Onay2',
|
title: translate('::ListForms.Workflow.Demo.Approval2'),
|
||||||
nextOnApprove: informId,
|
nextOnApprove: informId,
|
||||||
nextOnReject: informId,
|
nextOnReject: informId,
|
||||||
positionX: 586,
|
positionX: 586,
|
||||||
|
|
@ -252,7 +252,7 @@ function WizardStep6({
|
||||||
...normalizeCriteria(emptyCriteria('Inform', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Inform', listFormCode)),
|
||||||
id: informId,
|
id: informId,
|
||||||
nodeId: informId,
|
nodeId: informId,
|
||||||
title: 'Bilgilendirme1',
|
title: translate('::ListForms.Workflow.Demo.Inform'),
|
||||||
nextOnStart: endId,
|
nextOnStart: endId,
|
||||||
positionX: 458,
|
positionX: 458,
|
||||||
positionY: 336,
|
positionY: 336,
|
||||||
|
|
@ -261,7 +261,7 @@ function WizardStep6({
|
||||||
...normalizeCriteria(emptyCriteria('End', listFormCode)),
|
...normalizeCriteria(emptyCriteria('End', listFormCode)),
|
||||||
id: endId,
|
id: endId,
|
||||||
nodeId: endId,
|
nodeId: endId,
|
||||||
title: 'İş Akışı Bitir1',
|
title: translate('::ListForms.Workflow.Demo.End'),
|
||||||
positionX: 792,
|
positionX: 792,
|
||||||
positionY: 336,
|
positionY: 336,
|
||||||
} as WorkflowCriteriaDto,
|
} as WorkflowCriteriaDto,
|
||||||
|
|
@ -470,7 +470,7 @@ function WizardStep6({
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
|
||||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||||
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||||
{translate('::Back') || 'Back'}
|
{translate('::Back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 flex items-center justify-end gap-3">
|
<div className="flex-1 flex items-center justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -480,7 +480,7 @@ function WizardStep6({
|
||||||
icon={<FaArrowRight />}
|
icon={<FaArrowRight />}
|
||||||
onClick={continueToTodo}
|
onClick={continueToTodo}
|
||||||
>
|
>
|
||||||
{translate('::Next') || 'Next'}
|
{translate('::Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
collectLinks,
|
collectLinks,
|
||||||
getNodeOutcomes,
|
getNodeOutcomes,
|
||||||
outcomeLabel,
|
outcomeLabel,
|
||||||
|
OUTCOME_KEYS,
|
||||||
type WorkflowLink,
|
type WorkflowLink,
|
||||||
type WorkflowOutcome,
|
type WorkflowOutcome,
|
||||||
} from '@/utils/workflow/workflowHelpers'
|
} from '@/utils/workflow/workflowHelpers'
|
||||||
|
|
@ -93,6 +94,8 @@ export function WorkflowCanvas({
|
||||||
)
|
)
|
||||||
const links = useMemo(() => collectLinks(arrowCriteria) as WorkflowLink[], [arrowCriteria])
|
const links = useMemo(() => collectLinks(arrowCriteria) as WorkflowLink[], [arrowCriteria])
|
||||||
|
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key !== 'Delete') return
|
if (event.key !== 'Delete') return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
@ -124,14 +127,14 @@ export function WorkflowCanvas({
|
||||||
>
|
>
|
||||||
{pendingLink && (
|
{pendingLink && (
|
||||||
<div className="sticky left-2.5 top-2.5 z-50 m-2.5 inline-flex min-h-[34px] items-center rounded-md border border-[#8bb3f1] bg-[#eff6ff] px-3 text-[13px] text-blue-800 shadow-lg dark:border-blue-500/60 dark:bg-blue-900/40 dark:text-blue-100">
|
<div className="sticky left-2.5 top-2.5 z-50 m-2.5 inline-flex min-h-[34px] items-center rounded-md border border-[#8bb3f1] bg-[#eff6ff] px-3 text-[13px] text-blue-800 shadow-lg dark:border-blue-500/60 dark:bg-blue-900/40 dark:text-blue-100">
|
||||||
{outcomeLabel(pendingLink.outcome)} çıkışı seçildi. Hedef iş akışı adımına tıklayın.
|
{translate('::ListForms.Workflow.PendingLinkHint', {
|
||||||
|
outcome: translate('::' + (outcomeLabel(pendingLink.outcome) ?? '')),
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentCriteria.length === 0 && (
|
{currentCriteria.length === 0 && (
|
||||||
<div className="sticky left-[18px] top-[18px] z-30 inline-grid max-w-[360px] gap-1 rounded-lg border border-[#cfd6e2] bg-white/95 p-3.5 text-[#475467] shadow-lg dark:border-gray-700 dark:bg-gray-900/95 dark:text-gray-300">
|
<div className="sticky left-[18px] top-[18px] z-30 inline-grid max-w-[360px] gap-1 rounded-lg border border-[#cfd6e2] bg-white/95 p-3.5 text-[#475467] shadow-lg dark:border-gray-700 dark:bg-gray-900/95 dark:text-gray-300">
|
||||||
<span>
|
<span>{translate('::ListForms.Workflow.EmptyCanvasHint')}</span>
|
||||||
Üstteki butonlardan adım ekleyin, sonra çıkış etiketleriyle bağlantıları kurun.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -252,11 +255,7 @@ export function WorkflowCanvas({
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<ArrowLabel
|
<ArrowLabel key={`${link.key}-label`} link={link} pendingLink={pendingLink} />
|
||||||
key={`${link.key}-label`}
|
|
||||||
link={link}
|
|
||||||
pendingLink={pendingLink}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -387,7 +386,9 @@ function FlowNode({
|
||||||
onBeginLink(item.id, outcome.field)
|
onBeginLink(item.id, outcome.field)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{outcome.label}
|
{outcome.label.startsWith('ListForms.')
|
||||||
|
? translate('::' + outcome.label)
|
||||||
|
: outcome.label}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -503,13 +504,7 @@ function Arrow({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArrowLabel({
|
function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
|
||||||
link,
|
|
||||||
pendingLink,
|
|
||||||
}: {
|
|
||||||
link: WorkflowLink
|
|
||||||
pendingLink: PendingLink
|
|
||||||
}) {
|
|
||||||
if (!link.label) return null
|
if (!link.label) return null
|
||||||
|
|
||||||
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
|
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
|
||||||
|
|
@ -550,8 +545,8 @@ function ArrowLabel({
|
||||||
function linkTone(link: WorkflowLink) {
|
function linkTone(link: WorkflowLink) {
|
||||||
const field = link.sourcePort?.field || ''
|
const field = link.sourcePort?.field || ''
|
||||||
const label = link.label || ''
|
const label = link.label || ''
|
||||||
if (field === 'nextOnReject' || label === 'Red') return 'reject'
|
if (field === 'nextOnReject' || label === OUTCOME_KEYS.nextOnReject) return 'reject'
|
||||||
if (field === 'nextOnApprove' || label === 'Onay') return 'approve'
|
if (field === 'nextOnApprove' || label === OUTCOME_KEYS.nextOnApprove) return 'approve'
|
||||||
if (field.startsWith('compareOutcomes:') || link.source.kind === 'Compare') return 'compare'
|
if (field.startsWith('compareOutcomes:') || link.source.kind === 'Compare') return 'compare'
|
||||||
if (field === 'nextOnStart') return 'next'
|
if (field === 'nextOnStart') return 'next'
|
||||||
return 'neutral'
|
return 'neutral'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import {
|
||||||
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
|
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||||
import { SelectBoxOption } from '@/types/shared'
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
|
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
|
||||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||||
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
||||||
|
|
||||||
|
|
@ -48,9 +50,10 @@ export function WorkflowCriteria({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: WorkflowCriteriaProps) {
|
}: WorkflowCriteriaProps) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
const setField = (name: string, value: unknown) => onChange({ ...formValues, [name]: value })
|
const setField = (name: string, value: unknown) => onChange({ ...formValues, [name]: value })
|
||||||
const targetOptions = [
|
const targetOptions = [
|
||||||
{ value: '', label: 'Bağlantı yok' },
|
{ value: '', label: translate('::ListForms.Workflow.NoConnection') },
|
||||||
...criteria
|
...criteria
|
||||||
.filter((item) => item.id !== formValues.id)
|
.filter((item) => item.id !== formValues.id)
|
||||||
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
|
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
|
||||||
|
|
@ -164,7 +167,6 @@ export function WorkflowCriteria({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
const closeDialog = () => onSelect('')
|
const closeDialog = () => onSelect('')
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -189,7 +191,7 @@ export function WorkflowCriteria({
|
||||||
|
|
||||||
{criteria.map((item) => {
|
{criteria.map((item) => {
|
||||||
const isSelected = item.id === selectedId
|
const isSelected = item.id === selectedId
|
||||||
const connectionSummary = criteriaConnectionSummary(item, criteria)
|
const connectionSummary = criteriaConnectionSummary(item, criteria, translate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
|
|
@ -233,7 +235,9 @@ export function WorkflowCriteria({
|
||||||
'::' + kindOptions.find((option) => option.value === item.kind)?.value,
|
'::' + kindOptions.find((option) => option.value === item.kind)?.value,
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="min-w-[220px] break-words">{criteriaSummaryContent(item)}</Td>
|
<Td className="min-w-[220px] break-words">
|
||||||
|
{criteriaSummaryContent(item, translate)}
|
||||||
|
</Td>
|
||||||
<Td className="min-w-[220px] break-words">{connectionSummary}</Td>
|
<Td className="min-w-[220px] break-words">{connectionSummary}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)
|
)
|
||||||
|
|
@ -250,7 +254,10 @@ export function WorkflowCriteria({
|
||||||
<FormItem asterisk label={translate('::App.Platform.Type')}>
|
<FormItem asterisk label={translate('::App.Platform.Type')}>
|
||||||
<SelectField
|
<SelectField
|
||||||
required
|
required
|
||||||
options={kindOptions}
|
options={kindOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
label: translate('::' + option.label),
|
||||||
|
}))}
|
||||||
value={formValues.kind}
|
value={formValues.kind}
|
||||||
onChange={(value) => setField('kind', value)}
|
onChange={(value) => setField('kind', value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -263,10 +270,7 @@ export function WorkflowCriteria({
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
{(formValues.kind === 'Approval' || formValues.kind === 'Inform') && (
|
{(formValues.kind === 'Approval' || formValues.kind === 'Inform') && (
|
||||||
<FormItem
|
<FormItem asterisk label={translate('::App.Listform.ListformField.Approver')}>
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Approver')}
|
|
||||||
>
|
|
||||||
<MultiSelectField
|
<MultiSelectField
|
||||||
required
|
required
|
||||||
options={userList}
|
options={userList}
|
||||||
|
|
@ -334,7 +338,9 @@ export function WorkflowCriteria({
|
||||||
setField('compareOutcomes', [
|
setField('compareOutcomes', [
|
||||||
...(formValues.compareOutcomes || []),
|
...(formValues.compareOutcomes || []),
|
||||||
emptyCompareOutcome1(
|
emptyCompareOutcome1(
|
||||||
`Durum ${(formValues.compareOutcomes || []).length + 1}`,
|
translate('::ListForms.Workflow.OutcomeN', {
|
||||||
|
index: (formValues.compareOutcomes || []).length + 1,
|
||||||
|
}),
|
||||||
defaultCompareColumn,
|
defaultCompareColumn,
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
@ -364,7 +370,7 @@ export function WorkflowCriteria({
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
value={outcome.label}
|
value={outcome.label}
|
||||||
aria-label="Durum adı zorunlu"
|
aria-label={translate('::ListForms.Workflow.OutcomeNameRequired')}
|
||||||
className="flex-[5]"
|
className="flex-[5]"
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
updateCompareOutcome(index, {
|
updateCompareOutcome(index, {
|
||||||
|
|
@ -439,7 +445,7 @@ export function WorkflowCriteria({
|
||||||
className="flex-[1]"
|
className="flex-[1]"
|
||||||
onClick={() => removeCompareCondition(index, conditionIndex)}
|
onClick={() => removeCompareCondition(index, conditionIndex)}
|
||||||
>
|
>
|
||||||
Sil
|
{translate('::App.Platform.Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -571,7 +577,7 @@ function MultiSelectField({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
function criteriaSummaryContent(item: WorkflowCriteriaDto, translate: TranslateFn) {
|
||||||
if (item.kind === 'Compare') {
|
if (item.kind === 'Compare') {
|
||||||
const outcomes = item.compareOutcomes || []
|
const outcomes = item.compareOutcomes || []
|
||||||
if (!outcomes.length) return '-'
|
if (!outcomes.length) return '-'
|
||||||
|
|
@ -580,7 +586,9 @@ function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||||
<ul className="m-0 grid gap-1">
|
<ul className="m-0 grid gap-1">
|
||||||
{outcomes.map((outcome, index: number) => (
|
{outcomes.map((outcome, index: number) => (
|
||||||
<li key={`${outcome.label || 'outcome'}-${index}`}>
|
<li key={`${outcome.label || 'outcome'}-${index}`}>
|
||||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
<strong>
|
||||||
|
{outcome.label || translate('::ListForms.Workflow.OutcomeN', { index: index + 1 })}:
|
||||||
|
</strong>{' '}
|
||||||
{compareOutcomeRuleText(outcome)}
|
{compareOutcomeRuleText(outcome)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -591,7 +599,11 @@ function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||||
return criteriaSummary(item)
|
return criteriaSummary(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) {
|
function criteriaConnectionSummary(
|
||||||
|
item: WorkflowCriteriaDto,
|
||||||
|
criteria: WorkflowCriteriaDto[],
|
||||||
|
translate: TranslateFn,
|
||||||
|
) {
|
||||||
if (item.kind === 'Compare') {
|
if (item.kind === 'Compare') {
|
||||||
const outcomes = item.compareOutcomes || []
|
const outcomes = item.compareOutcomes || []
|
||||||
if (!outcomes.length) return '-'
|
if (!outcomes.length) return '-'
|
||||||
|
|
@ -600,7 +612,9 @@ function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: Workflow
|
||||||
<ul className="m-0 grid gap-1">
|
<ul className="m-0 grid gap-1">
|
||||||
{outcomes.map((outcome, index: number) => (
|
{outcomes.map((outcome, index: number) => (
|
||||||
<li key={`${outcome.label || 'target'}-${index}`}>
|
<li key={`${outcome.label || 'target'}-${index}`}>
|
||||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
<strong>
|
||||||
|
{outcome.label || translate('::ListForms.Workflow.OutcomeN', { index: index + 1 })}:
|
||||||
|
</strong>{' '}
|
||||||
{targetTitle(criteria, outcome.targetId)}
|
{targetTitle(criteria, outcome.targetId)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -612,10 +626,12 @@ function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: Workflow
|
||||||
return (
|
return (
|
||||||
<ul className="m-0 grid gap-1">
|
<ul className="m-0 grid gap-1">
|
||||||
<li>
|
<li>
|
||||||
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)}
|
<strong>{translate('::ListForms.Workflow.Approve')}:</strong>{' '}
|
||||||
|
{targetTitle(criteria, item.nextOnApprove)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)}
|
<strong>{translate('::ListForms.Workflow.Reject')}:</strong>{' '}
|
||||||
|
{targetTitle(criteria, item.nextOnReject)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ function DesignerToolbar({
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(designerButtonClass, designerNeutralColorClass)}
|
className={classNames(designerButtonClass, designerNeutralColorClass)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
title="Demo akışı yükle"
|
title={translate('::ListForms.Workflow.LoadDemoFlow')}
|
||||||
onClick={onResetDemo}
|
onClick={onResetDemo}
|
||||||
>
|
>
|
||||||
<FiRefreshCw />
|
<FiRefreshCw />
|
||||||
|
|
@ -209,7 +209,7 @@ function DesignerToolbar({
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(designerButtonClass, designerAccentColorClass)}
|
className={classNames(designerButtonClass, designerAccentColorClass)}
|
||||||
disabled={busy || currentCriteria.length === 0}
|
disabled={busy || currentCriteria.length === 0}
|
||||||
title="Düğümleri okunabilir şekilde yerleştir"
|
title={translate('::ListForms.Workflow.FitLayout')}
|
||||||
onClick={onFitLayout}
|
onClick={onFitLayout}
|
||||||
>
|
>
|
||||||
<FiMaximize2 />
|
<FiMaximize2 />
|
||||||
|
|
@ -220,7 +220,7 @@ function DesignerToolbar({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
||||||
title="Yakınlaştır"
|
title={translate('::App.Platform.ZoomIn')}
|
||||||
onClick={onZoomIn}
|
onClick={onZoomIn}
|
||||||
>
|
>
|
||||||
<FiZoomIn />
|
<FiZoomIn />
|
||||||
|
|
@ -230,7 +230,7 @@ function DesignerToolbar({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
||||||
title="Uzaklaştır"
|
title={translate('::App.Platform.ZoomOut')}
|
||||||
onClick={onZoomOut}
|
onClick={onZoomOut}
|
||||||
>
|
>
|
||||||
<FiZoomOut />
|
<FiZoomOut />
|
||||||
|
|
@ -269,7 +269,11 @@ function DesignerTabs({
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
<div
|
||||||
|
className="inline-flex gap-1 rounded-lg"
|
||||||
|
role="tablist"
|
||||||
|
aria-label={translate('::ListForms.Workflow.FlowDesign')}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ const OrganizationUnits = () => {
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{translate('::Kaydet')}
|
{translate('::Save')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -234,7 +234,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -291,7 +291,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -327,7 +327,7 @@ const OrganizationUnits = () => {
|
||||||
await ouPut({ id, displayName: name })
|
await ouPut({ id, displayName: name })
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{translate('::Kaydet')}
|
{translate('::Save')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -338,7 +338,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -360,7 +360,7 @@ const OrganizationUnits = () => {
|
||||||
await ouPutMove(id, newParentId)
|
await ouPutMove(id, newParentId)
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{translate('::Kaydet')}
|
{translate('::Save')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -371,7 +371,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -428,8 +428,8 @@ const OrganizationUnits = () => {
|
||||||
<div className="file-actions">
|
<div className="file-actions">
|
||||||
<div className="flex gap-1 folderFileActions">
|
<div className="flex gap-1 folderFileActions">
|
||||||
<Button
|
<Button
|
||||||
size='xs'
|
size="xs"
|
||||||
variant='plain'
|
variant="plain"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
className="!h-6 !w-6 !bg-blue-50 !p-0 text-blue-600 hover:!bg-blue-100 hover:text-blue-700 dark:!bg-blue-900/30 dark:text-blue-300 dark:hover:!bg-blue-800/60 dark:hover:text-blue-100"
|
className="!h-6 !w-6 !bg-blue-50 !p-0 text-blue-600 hover:!bg-blue-100 hover:text-blue-700 dark:!bg-blue-900/30 dark:text-blue-300 dark:hover:!bg-blue-800/60 dark:hover:text-blue-100"
|
||||||
icon={<FaUserPlus className="text-xs" />}
|
icon={<FaUserPlus className="text-xs" />}
|
||||||
|
|
@ -437,8 +437,8 @@ const OrganizationUnits = () => {
|
||||||
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
|
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
|
||||||
></Button>
|
></Button>
|
||||||
<Button
|
<Button
|
||||||
size='xs'
|
size="xs"
|
||||||
variant='plain'
|
variant="plain"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
className="!h-6 !w-6 !bg-teal-50 !p-0 text-teal-600 hover:!bg-teal-100 hover:text-teal-700 dark:!bg-teal-900/30 dark:text-teal-300 dark:hover:!bg-teal-800/60 dark:hover:text-teal-100"
|
className="!h-6 !w-6 !bg-teal-50 !p-0 text-teal-600 hover:!bg-teal-100 hover:text-teal-700 dark:!bg-teal-900/30 dark:text-teal-300 dark:hover:!bg-teal-800/60 dark:hover:text-teal-100"
|
||||||
icon={<FaEdit className="text-xs" />}
|
icon={<FaEdit className="text-xs" />}
|
||||||
|
|
@ -446,8 +446,8 @@ const OrganizationUnits = () => {
|
||||||
title={translate('::Abp.Identity.OrganizationUnit.Rename')}
|
title={translate('::Abp.Identity.OrganizationUnit.Rename')}
|
||||||
></Button>
|
></Button>
|
||||||
<Button
|
<Button
|
||||||
size='xs'
|
size="xs"
|
||||||
variant='plain'
|
variant="plain"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
className="!h-6 !w-6 !bg-red-50 !p-0 text-red-600 hover:!bg-red-100 hover:text-red-700 dark:!bg-red-900/30 dark:text-red-300 dark:hover:!bg-red-800/60 dark:hover:text-red-100"
|
className="!h-6 !w-6 !bg-red-50 !p-0 text-red-600 hover:!bg-red-100 hover:text-red-700 dark:!bg-red-900/30 dark:text-red-300 dark:hover:!bg-red-800/60 dark:hover:text-red-100"
|
||||||
icon={<FaTrashAlt className="text-xs" />}
|
icon={<FaTrashAlt className="text-xs" />}
|
||||||
|
|
@ -562,9 +562,9 @@ const OrganizationUnits = () => {
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
<Th>Adı Soyadı</Th>
|
<Th>{translate('::App.Platform.FullName')}</Th>
|
||||||
<Th>E-Posta</Th>
|
<Th>{translate('::Abp.Account.EmailAddress')}</Th>
|
||||||
<Th>Durum</Th>
|
<Th>{translate('::App.Platform.Status')}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
)}
|
)}
|
||||||
|
|
@ -633,7 +633,7 @@ const OrganizationUnits = () => {
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
<Th>Rol</Th>
|
<Th>{translate('::App.Platform.Role')}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
)}
|
)}
|
||||||
|
|
@ -786,7 +786,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -857,7 +857,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -935,7 +935,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -984,7 +984,7 @@ const OrganizationUnits = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
Hata
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
|
||||||
|
|
@ -89,15 +89,15 @@ const General = () => {
|
||||||
const maxFileSize = 2000000
|
const maxFileSize = 2000000
|
||||||
|
|
||||||
if (fileList.length >= 1) {
|
if (fileList.length >= 1) {
|
||||||
return `Sadece bir dosya seçebilirsiniz`
|
return translate('::App.Profile.OnlyOneFile')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files) {
|
if (files) {
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
if (!allowedFileType.includes(f.type)) {
|
if (!allowedFileType.includes(f.type)) {
|
||||||
valid = '.jpg, .jpeg, .gif veya .png yükleyebilirsiniz'
|
valid = translate('::App.Profile.AllowedImageTypes')
|
||||||
} else if (f.size >= maxFileSize) {
|
} else if (f.size >= maxFileSize) {
|
||||||
valid = 'En fazla 2mb dosya yükleyebilirsiniz'
|
valid = translate('::App.Profile.MaxFileSize')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ const General = () => {
|
||||||
avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`,
|
avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.push(<Notification title={'Profil güncellendi'} type="success" />, {
|
toast.push(<Notification title={translate('::App.Profile.Updated')} type="success" />, {
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -178,31 +178,31 @@ const General = () => {
|
||||||
const educationOptions: SelectBoxOption[] = [
|
const educationOptions: SelectBoxOption[] = [
|
||||||
{
|
{
|
||||||
value: 'İlkokul',
|
value: 'İlkokul',
|
||||||
label: translate('::App.EducationLevel.Primary') || 'İlkokul',
|
label: translate('::App.EducationLevel.Primary'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Ortaokul',
|
value: 'Ortaokul',
|
||||||
label: translate('::App.EducationLevel.MiddleSchool') || 'Ortaokul',
|
label: translate('::App.EducationLevel.MiddleSchool'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Lise',
|
value: 'Lise',
|
||||||
label: translate('::App.EducationLevel.HighSchool') || 'Lise',
|
label: translate('::App.EducationLevel.HighSchool'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Ön Lisans',
|
value: 'Ön Lisans',
|
||||||
label: translate('::App.EducationLevel.Associate') || 'Ön Lisans',
|
label: translate('::App.EducationLevel.Associate'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Lisans',
|
value: 'Lisans',
|
||||||
label: translate('::App.EducationLevel.Bachelor') || 'Lisans',
|
label: translate('::App.EducationLevel.Bachelor'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Yüksek Lisans',
|
value: 'Yüksek Lisans',
|
||||||
label: translate('::App.EducationLevel.Master') || 'Yüksek Lisans',
|
label: translate('::App.EducationLevel.Master'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Doktora',
|
value: 'Doktora',
|
||||||
label: translate('::App.EducationLevel.PhD') || 'Doktora',
|
label: translate('::App.EducationLevel.PhD'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ const CreateTenantFromOrderDialog = ({
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2500}>
|
<Notification type="success" duration={2500}>
|
||||||
{translate('::App.Platform.Success') || 'Tenant kaydedildi'}
|
{translate('::App.Platform.Success')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -95,7 +95,7 @@ const CreateTenantFromOrderDialog = ({
|
||||||
(error as any)?.response?.data?.error?.message ??
|
(error as any)?.response?.data?.error?.message ??
|
||||||
(error as any)?.response?.data?.message ??
|
(error as any)?.response?.data?.message ??
|
||||||
(error as any)?.message ??
|
(error as any)?.message ??
|
||||||
'Islem basarisiz'
|
translate('::App.Platform.OperationFailed')
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={5000}>
|
<Notification type="danger" duration={5000}>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ function TenantConnectionString({
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
{'Hata'}
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -136,7 +136,7 @@ function TenantConnectionString({
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" duration={2000}>
|
<Notification type="danger" duration={2000}>
|
||||||
{'Hata'}
|
{translate('::App.Platform.Error')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -187,7 +187,7 @@ function TenantConnectionString({
|
||||||
(error as any)?.response?.data?.error?.message ??
|
(error as any)?.response?.data?.error?.message ??
|
||||||
(error as any)?.response?.data?.message ??
|
(error as any)?.response?.data?.message ??
|
||||||
(error as any)?.message ??
|
(error as any)?.message ??
|
||||||
'İşlem başarısız'
|
translate('::App.Platform.OperationFailed')
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="warning" duration={10000}>
|
<Notification type="warning" duration={10000}>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue