ScrollBar ve Fields EditorOptions

This commit is contained in:
Sedat ÖZTÜRK 2026-07-01 10:39:32 +03:00
parent e8295fd449
commit f3a516eb92
6 changed files with 391 additions and 213 deletions

View file

@ -4104,7 +4104,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions = EncodeHtmlJson
EditorOptions = EncodeHtmlJson(false)
},
new() {
ListFormCode = listForm.ListFormCode,

View file

@ -49,7 +49,7 @@ public static class ListFormSeeder_DefaultJsons
});
public static readonly string DefaultFilterJson = "\"IsDeleted\" = 'false'";
public static readonly string EncodeHtmlJson = "{ \"encodeHtml\" = false }";
public static string EncodeHtmlJson(bool value) => JsonSerializer.Serialize(new { encodeHtml = value });
public static string DefaultFilterRowJson(bool visible = true) => JsonSerializer.Serialize(new GridFilterRowDto { Visible = visible });
public static string DefaultHeaderFilterJson(bool visible = true) => JsonSerializer.Serialize(new { Visible = visible });
public static string DefaultSearchPanelJson(bool visible = true) => JsonSerializer.Serialize(new { Visible = visible });

View file

@ -3,6 +3,71 @@
@tailwind utilities;
@layer base {
:root,
.light {
--app-scrollbar-size: 8px;
--app-scrollbar-thumb: #9ca3af;
--app-scrollbar-thumb-hover: #6b7280;
--app-scrollbar-track: transparent;
}
.dark {
--app-scrollbar-thumb: #4b5563;
--app-scrollbar-thumb-hover: #6b7280;
}
* {
scrollbar-color: var(--app-scrollbar-thumb) var(--app-scrollbar-track);
scrollbar-width: thin;
}
*::-webkit-scrollbar {
width: var(--app-scrollbar-size);
height: var(--app-scrollbar-size);
}
*::-webkit-scrollbar-track {
background: var(--app-scrollbar-track);
}
*::-webkit-scrollbar-thumb {
min-height: 40px;
border: 2px solid transparent;
border-radius: 999px;
background-color: var(--app-scrollbar-thumb);
background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--app-scrollbar-thumb-hover);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
.dx-scrollable-scrollbar.dx-scrollbar-vertical,
.dx-scrollable-scrollbar.dx-scrollbar-vertical .dx-scrollable-scroll {
width: var(--app-scrollbar-size) !important;
}
.dx-scrollable-scrollbar.dx-scrollbar-horizontal,
.dx-scrollable-scrollbar.dx-scrollbar-horizontal .dx-scrollable-scroll {
height: var(--app-scrollbar-size) !important;
}
.dx-scrollable-scroll {
border-radius: 999px !important;
background-color: var(--app-scrollbar-thumb) !important;
}
.dx-scrollable-scroll:hover,
.dx-scrollable-scrollbar.dx-state-hover .dx-scrollable-scroll,
.dx-scrollable-scrollbar.dx-scrollbar-hoverable.dx-scrollable-scrollbar-active
.dx-scrollable-scroll {
background-color: var(--app-scrollbar-thumb-hover) !important;
}
body {
@apply text-gray-500 dark:text-gray-400 text-xs bg-gray-100 dark:bg-gray-900 leading-normal;
-webkit-font-smoothing: antialiased;

View file

@ -10,7 +10,14 @@ export interface ScrollbarProps extends ReactCustomScrollbarProps {
export type ScrollbarRef = Scrollbars
const ScrollBar = forwardRef<ScrollbarRef, ScrollbarProps>((props, ref) => {
const { direction = 'ltr', ...rest } = props
const {
direction = 'ltr',
renderThumbVertical,
renderThumbHorizontal,
renderTrackVertical,
renderTrackHorizontal,
...rest
} = props
return (
<Scrollbars
@ -27,6 +34,65 @@ const ScrollBar = forwardRef<ScrollbarRef, ScrollbarProps>((props, ref) => {
}}
/>
)}
renderTrackVertical={
renderTrackVertical ||
((props) => (
<div
{...props}
style={{
...props.style,
top: 0,
right: direction === 'rtl' ? 'auto' : 0,
left: direction === 'rtl' ? 0 : 'auto',
bottom: 0,
width: 'var(--app-scrollbar-size)',
backgroundColor: 'var(--app-scrollbar-track)',
}}
/>
))
}
renderTrackHorizontal={
renderTrackHorizontal ||
((props) => (
<div
{...props}
style={{
...props.style,
right: 0,
left: 0,
bottom: 0,
height: 'var(--app-scrollbar-size)',
backgroundColor: 'var(--app-scrollbar-track)',
}}
/>
))
}
renderThumbVertical={
renderThumbVertical ||
((props) => (
<div
{...props}
style={{
...props.style,
borderRadius: 999,
backgroundColor: 'var(--app-scrollbar-thumb)',
}}
/>
))
}
renderThumbHorizontal={
renderThumbHorizontal ||
((props) => (
<div
{...props}
style={{
...props.style,
borderRadius: 999,
backgroundColor: 'var(--app-scrollbar-thumb)',
}}
/>
))
}
{...rest}
/>
)

View file

@ -1,11 +1,13 @@
import { Container } from '@/components/shared'
import { Button, Card, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { Button, Card, FormItem, Input, Select } from '@/components/ui'
import { ColumnFormatEditDto, ListFormFieldEditTabs } from '@/proxy/admin/list-form-field/models'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Field, FieldProps, Form, Formik } from 'formik'
import { useState } from 'react'
import { FaSlidersH } from 'react-icons/fa'
import { number, object, string } from 'yup'
import { dbSourceTypeOptions, listFormAlignmentOptions } from '../options'
import EditorOptionsBuilderDialog from '../json-row-operations/EditorOptionsBuilderDialog'
import { FormFieldEditProps } from './FormFields'
import { tooltipFormatListOptions } from '@/proxy/admin/list-form/options'
@ -15,6 +17,9 @@ const schema = object().shape({
placeHolder: string(),
bandName: string(),
sourceDbType: number().required(),
alignment: string(),
format: string(),
editorOptions: string().max(1000),
})
function FormFieldTabDetails({
@ -26,6 +31,7 @@ function FormFieldTabDetails({
initialValues: ColumnFormatEditDto
} & FormFieldEditProps) {
const { translate } = useLocalization()
const [isEditorOptionsDialogOpen, setIsEditorOptionsDialogOpen] = useState(false)
return (
<Formik
@ -36,7 +42,7 @@ function FormFieldTabDetails({
await onSubmit(ListFormFieldEditTabs.DetailsForm, values, formikHelpers)
}}
>
{({ touched, errors, isSubmitting, values }) => (
{({ touched, errors, isSubmitting, values, setFieldValue }) => (
<Form>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card>
@ -123,16 +129,33 @@ function FormFieldTabDetails({
invalid={errors.editorOptions && touched.editorOptions}
errorMessage={errors.editorOptions}
>
<Field
type="text"
name="editorOptions"
component={Input}
rows={4}
textArea={true}
/>
<div className="flex gap-2 items-start">
<Field
type="text"
name="editorOptions"
component={Input}
rows={4}
textArea={true}
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Build editor options"
icon={<FaSlidersH />}
onClick={() => setIsEditorOptionsDialogOpen(true)}
/>
</div>
</FormItem>
</Card>
</div>
<EditorOptionsBuilderDialog
isOpen={isEditorOptionsDialogOpen}
value={values.editorOptions}
onClose={() => setIsEditorOptionsDialogOpen(false)}
onApply={(val) => setFieldValue('editorOptions', val)}
/>
<Button size="sm" className="mt-1" block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>

View file

@ -19,7 +19,7 @@ type EditorOptionsBuilderDialogProps = {
}
const baseInputClass =
'w-full 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'
'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'
const boolOptions = [
{ key: 'showClearButton', label: 'showClearButton' },
@ -145,6 +145,18 @@ function toSelectValue(value: unknown): string {
return typeof value === 'string' ? value : ''
}
function toEncodeHtmlSelectValue(value: unknown): string {
if (value === true) return 'true'
if (value === false) return 'false'
return value === 'auto' ? 'auto' : ''
}
function readEncodeHtmlSelectValue(value: string) {
if (value === 'true') return true
if (value === 'false') return false
return undefined
}
function readCustomValue(option: CustomOption) {
if (option.type === 'boolean') return option.value === 'true'
if (option.type === 'number') return Number(option.value || 0)
@ -278,23 +290,182 @@ function EditorOptionsBuilderDialog({
return isEmptyObject(next) ? '' : JSON.stringify(next, null, 2)
}, [customOptions, options])
const presetButtons = (
<div className="flex flex-wrap gap-2">
<Button
type="button"
size="sm"
onClick={clearOptions}
title="editorOptions alanını boşaltır. Backend'e boş string kaydedilir."
>
NULL / Empty
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ disabled: true })}
title='{"disabled": true} ekler.'
>
disabled
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ tooltip: { enabled: true } })}
title='{"tooltip": {"enabled": true}} ekler.'
>
Tooltip
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ format: { type: 'fixedPoint', precision: 2 } })}
title="Ondalık format precision 2 ekler."
>
fixedPoint precision 2
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
disabled: true,
format: { type: 'fixedPoint', precision: 2 },
})
}
title="Pasif ve precision 2 fixedPoint formatını birlikte ekler."
>
disabled fixedPoint 2
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: { type: 'fixedPoint', precision: 1 },
useMaskBehavior: true,
showSpinButtons: true,
disabled: true,
})
}
title="Precision 1, mask behavior, spin buttons ve pasif ayarını ekler."
>
disabled fixedPoint 1
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: { type: 'fixedPoint', precision: 2 },
useMaskBehavior: true,
showSpinButtons: true,
})
}
title="NumberBox için precision 2, mask behavior ve spin buttons ekler."
>
numberSpin precision 2
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ format: 'dd/MM/yyyy', displayFormat: 'dd/MM/yyyy' })}
title="DateBox için gün/ay/yıl formatını ekler."
>
date dd/MM/yyyy
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: 'dd/MM/yyyy HH:mm',
displayFormat: 'dd/MM/yyyy HH:mm',
})
}
title="DateBox için gün/ay/yıl saat:dakika formatını ekler."
>
dateTime dd/MM/yyyy HH:mm
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: 'phoneGlobal',
mask: '(000) 000-0000',
maskInvalidMessage: 'Lütfen geçerli bir telefon numarası girin',
useMaskedValue: false,
maskRules: { X: '[0-9]' },
placeholder: '(555) 123-4567',
})
}
title="Telefon maskesi, yer tutucu ve Türkçe hata mesajı ekler."
>
phoneGlobal mask
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
type: 'time',
pickerType: 'list',
displayFormat: 'HH:mm',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
interval: 5,
width: '100%',
})
}
title="Saat seçimi için dxDateBox time picker ayarları ekler."
>
time picker
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ width: 80, height: 80, multiple: true })}
title="ImageUpload için 80x80 ve multiple ayarlarını ekler."
>
multi image
</Button>
<Button type="button" size="sm" onClick={() => addPreset({ height: 60 })} title='{"height": 60} ekler.'>
Height 60
</Button>
<Button type="button" size="sm" onClick={() => addPreset({ height: 100 })} title='{"height": 100} ekler.'>
Height 100
</Button>
<Button type="button" size="sm" onClick={() => addPreset({ height: 200 })} title='{"height": 200} ekler.'>
Height 200
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset(buildHtmlEditorOptions())}
title="HtmlEditor toolbar, image upload ve media resizing ayarlarını ekler."
>
htmlEditor toolbar
</Button>
</div>
)
return (
<Dialog
isOpen={isOpen}
width={980}
height="82vh"
width={1320}
height="88vh"
contentClassName="flex flex-col"
style={{ overlay: { zIndex: 1300 } }}
preventScroll
onClose={onClose}
onRequestClose={onClose}
>
<Dialog.Body className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
<Dialog.Body className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
<div className="flex items-center justify-between gap-3">
<h5
title={`${
editorType || 'DevExpress editor'
} için DevExtreme editorOptions JSON'u oluşturur. Soldan hazır ayarları seç, sağdaki JSON önizleme alanından sonucu kontrol et.`}
} için DevExtreme editorOptions JSON'u oluşturur. Üstten hazır ayarları seç, sağdaki JSON önizleme alanından sonucu kontrol et.`}
>
Editor Options Builder
</h5>
@ -306,8 +477,18 @@ function EditorOptionsBuilderDialog({
</div>
)}
<div className="grid grid-cols-2 min-h-0 gap-4 overflow-y-auto pr-1 ">
<section className="flex flex-col gap-4">
<section className="rounded border border-gray-200 dark:border-gray-700 bg-gray-50/70 dark:bg-gray-900/30 p-3">
<div
className="mb-3 text-sm font-semibold"
title="Daha önce kullandığın hazır editorOptions örnekleri. Tıkladığın preset mevcut JSON ile birleştirilir, yani başka seçenekleri silmeden üzerine ekler."
>
Hazır Ayarlar
</div>
{presetButtons}
</section>
<div className="grid grid-cols-12 min-h-0 gap-4 overflow-hidden">
<section className="col-span-7 flex min-h-0 flex-col gap-3 overflow-y-auto pr-1">
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
@ -315,21 +496,39 @@ function EditorOptionsBuilderDialog({
>
1. Hızlı Anahtarlar
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
<div className="grid grid-cols-2 gap-x-3 gap-y-2">
{boolOptions.map((option) => (
<label
key={option.key}
className="flex items-center gap-2 rounded px-1 py-1 text-sm"
className="flex min-w-0 items-center gap-2 rounded px-1 py-1 text-xs"
title={`${option.key}: true/false olarak editorOptions içine yazılır.`}
>
<input
className="shrink-0"
type="checkbox"
checked={options[option.key] === true}
onChange={(event) => toggleOption(option.key, event.target.checked)}
/>
<span>{option.label}</span>
<span className="min-w-0 truncate">{option.label}</span>
</label>
))}
<label
className="col-span-2 grid min-w-0 grid-cols-[110px_1fr] items-center gap-2 rounded px-1 py-1 text-xs"
title="encodeHtml değerini true, false veya auto olarak editorOptions içine yazar."
>
<span className="min-w-0 truncate">encodeHtml</span>
<select
className={baseInputClass}
value={toEncodeHtmlSelectValue(options.encodeHtml)}
onChange={(event) =>
setOptionValue('encodeHtml', readEncodeHtmlSelectValue(event.target.value))
}
>
<option value="">Seç</option>
<option value="true">true</option>
<option value="false">false</option>
</select>
</label>
</div>
</div>
@ -340,7 +539,7 @@ function EditorOptionsBuilderDialog({
>
2. Boyut ve Görünüm
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 [&>label]:min-w-0">
<label className="text-xs text-gray-500">
{translate('::ListForms.ListFormEdit.DetailsWidth')}
<input
@ -387,7 +586,7 @@ function EditorOptionsBuilderDialog({
>
3. Genel Editor Davranışı
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 [&>label]:min-w-0">
<label className="text-xs text-gray-500">
label
<input
@ -469,7 +668,7 @@ function EditorOptionsBuilderDialog({
>
4. TextBox / Mask
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 [&>label]:min-w-0">
<label className="text-xs text-gray-500">
mode
<select
@ -557,7 +756,7 @@ function EditorOptionsBuilderDialog({
>
5. NumberBox
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 [&>label]:min-w-0">
{['min', 'max', 'step'].map((key) => (
<label key={key} className="text-xs text-gray-500">
{key}
@ -617,7 +816,7 @@ function EditorOptionsBuilderDialog({
>
6. DateBox
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 [&>label]:min-w-0">
<label className="text-xs text-gray-500">
type
<select
@ -791,195 +990,20 @@ function EditorOptionsBuilderDialog({
</div>
</section>
<section className="rounded border border-gray-200 dark:border-gray-700 p-3 flex flex-col min-h-[420px]">
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
title="Daha önce kullandığın hazır editorOptions örnekleri. Tıkladığın preset mevcut JSON ile birleştirilir, yani başka seçenekleri silmeden üzerine ekler."
>
Hazır Ayarlar
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
size="sm"
onClick={clearOptions}
title="editorOptions alanını boşaltır. Backend'e boş string kaydedilir."
>
NULL / Empty
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ disabled: true })}
title='{"disabled": true} ekler.'
>
disabled
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ tooltip: { enabled: true } })}
title='{"tooltip": {"enabled": true}} ekler.'
>
Tooltip
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ format: { type: 'fixedPoint', precision: 2 } })}
title="Ondalık format precision 2 ekler."
>
fixedPoint precision 2
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
disabled: true,
format: { type: 'fixedPoint', precision: 2 },
})
}
title="Pasif ve precision 2 fixedPoint formatını birlikte ekler."
>
disabled fixedPoint 2
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: { type: 'fixedPoint', precision: 1 },
useMaskBehavior: true,
showSpinButtons: true,
disabled: true,
})
}
title="Precision 1, mask behavior, spin buttons ve pasif ayarını ekler."
>
disabled fixedPoint 1
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: { type: 'fixedPoint', precision: 2 },
useMaskBehavior: true,
showSpinButtons: true,
})
}
title="NumberBox için precision 2, mask behavior ve spin buttons ekler."
>
numberSpin precision 2
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ format: 'dd/MM/yyyy', displayFormat: 'dd/MM/yyyy' })}
title="DateBox için gün/ay/yıl formatını ekler."
>
date dd/MM/yyyy
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: 'dd/MM/yyyy HH:mm',
displayFormat: 'dd/MM/yyyy HH:mm',
})
}
title="DateBox için gün/ay/yıl saat:dakika formatını ekler."
>
dateTime dd/MM/yyyy HH:mm
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
format: 'phoneGlobal',
mask: '(000) 000-0000',
maskInvalidMessage: 'Lütfen geçerli bir telefon numarası girin',
useMaskedValue: false,
maskRules: { X: '[0-9]' },
placeholder: '(555) 123-4567',
})
}
title="Telefon maskesi, yer tutucu ve Türkçe hata mesajı ekler."
>
phoneGlobal mask
</Button>
<Button
type="button"
size="sm"
onClick={() =>
addPreset({
type: 'time',
pickerType: 'list',
displayFormat: 'HH:mm',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
interval: 5,
width: '100%',
})
}
title="Saat seçimi için dxDateBox time picker ayarları ekler."
>
time picker
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ width: 80, height: 80, multiple: true })}
title="ImageUpload için 80x80 ve multiple ayarlarını ekler."
>
multi image
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ height: 60 })}
title='{"height": 60} ekler.'
>
Height 60
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ height: 100 })}
title='{"height": 100} ekler.'
>
Height 100
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset({ height: 200 })}
title='{"height": 200} ekler.'
>
Height 200
</Button>
<Button
type="button"
size="sm"
onClick={() => addPreset(buildHtmlEditorOptions())}
title="HtmlEditor toolbar, image upload ve media resizing ayarlarını ekler."
>
htmlEditor toolbar
</Button>
</div>
</div>
<section className="col-span-5 flex min-h-0 flex-col rounded border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<div
className="flex items-center gap-2 text-sm font-semibold mt-3"
className="mb-3 flex items-center justify-between gap-2 text-sm font-semibold"
title="Kaydet/Uygula sonrası editorOptions alanına yazılacak net JSON budur."
>
<FaCode />
JSON Önizleme
<span className="flex items-center gap-2">
<FaCode />
JSON Önizleme
</span>
<span className="rounded bg-gray-100 px-2 py-1 text-[11px] font-normal text-gray-500 dark:bg-gray-800 dark:text-gray-300">
{preview ? `${preview.length} karakter` : 'boş'}
</span>
</div>
<pre className="flex overflow-auto rounded bg-gray-50 dark:bg-gray-900 p-3 text-xs whitespace-pre-wrap">
<pre className="min-h-0 flex-1 overflow-auto rounded border border-gray-200 bg-gray-950 p-4 font-mono text-xs leading-5 text-emerald-100 shadow-inner dark:border-gray-700 whitespace-pre-wrap">
{preview || '{}'}
</pre>
</section>