ListFormEdit ekranında sütunların sıralaması

This commit is contained in:
Sedat ÖZTÜRK 2026-10-02 10:18:23 +03:00
parent 7be0b4ed88
commit d331fb62d4
4 changed files with 369 additions and 118 deletions

View file

@ -74903,6 +74903,20 @@
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ExpectedKm",
"Value": "Olması Gereken Km"
},
{
"Id": "a4130afd-d823-57b0-91e8-c0c11369801d",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormField.DragToReorder",
"Value": "Drag to reorder"
},
{
"Id": "59388a6c-8dcf-583e-b706-896644410dfe",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormField.DragToReorder",
"Value": "Sürükleyerek sırala"
}
]
}

View file

@ -1,6 +1,5 @@
import { AdaptableCard, Container } from '@/components/shared'
import {
Avatar,
Button,
Checkbox,
Dialog,
@ -8,12 +7,12 @@ import {
FormItem,
Input,
Notification,
Segment,
Table,
toast,
} from '@/components/ui'
import TBody from '@/components/ui/Table/TBody'
import THead from '@/components/ui/Table/THead'
import Td from '@/components/ui/Table/Td'
import Th from '@/components/ui/Table/Th'
import Tr from '@/components/ui/Table/Tr'
import {
@ -26,6 +25,7 @@ import {
import {
ColumnFormatEditDto,
CopyListFormFieldColumnDto,
ListFormFieldEditTabs,
} from '@/proxy/admin/list-form-field/models'
import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store'
@ -33,8 +33,24 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import setNull from '@/utils/setNull'
import classNames from 'classnames'
import { Field, Form, Formik, FormikHelpers } from 'formik'
import { useCallback, useEffect, useState } from 'react'
import { FaFileMedical, FaCopy, FaEyeSlash, FaMinus, FaTimes, FaTable } from 'react-icons/fa'
import { useCallback, useEffect, useRef, useState } from 'react'
import { FaFileMedical, FaCopy, FaTable } from 'react-icons/fa'
import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import SortableFieldRow from './SortableFieldRow'
import { FieldOrderMode, reorderFields, sortFields } from './fieldOrder'
import { number, object, string } from 'yup'
import FormFieldEdit from './FormFieldEdit'
import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options'
@ -85,7 +101,7 @@ function FormFields({
columns: DatabaseColumnDto[]
}) {
const { translate } = useLocalization()
const { canCreate, canDelete } = useFieldPermission()
const { canCreate, canUpdate, canDelete } = useFieldPermission()
const [fields, setFields] = useState<ColumnFormatEditDto[]>()
const [deleteRowId, setDeleteRowId] = useState<string | null>(null)
const [row, setRow] = useState<ColumnFormatEditDto | null>(null)
@ -93,6 +109,14 @@ function FormFields({
const [isCopyField, setIsCopyField] = useState(false)
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
const cultureName = useStoreState((state) => state.locale.currentLang)
const [orderMode, setOrderMode] = useState<FieldOrderMode>('list')
const [isSavingOrder, setIsSavingOrder] = useState(false)
// Siralama kaydedildikten sonra liste kullanicinin son uyguladigi filtreyle yeniden okunur.
const filterRef = useRef<{ cultureName?: string; roleId?: string; userId?: string }>({})
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
)
// ── All DB Columns (for "Tüm Sütunları Ekle") ───────────────────────────────
const listFormValues = useStoreState((s) => s.admin.lists.values)
@ -308,10 +332,50 @@ function FormFields({
}
}
const onDragEnd = async ({ active, over }: DragEndEvent) => {
if (!fields || !over) {
return
}
const changed = reorderFields(fields, orderMode, String(active.id), String(over.id))
if (changed.length === 0) {
return
}
setFields(fields.map((f) => changed.find((c) => c.id === f.id) ?? f))
setIsSavingOrder(true)
try {
// Her kayit wizard seed dosyasini yeniden yazar; paralel istek ayni dosyada yarisirdi.
for (const field of changed) {
await putListFormField({ ...field, editType: ListFormFieldEditTabs.OptionsForm })
}
toast.push(
<Notification type="success" duration={2000}>
{translate('::App.Platform.FormBilgileriKaydedildi')}
</Notification>,
{ placement: 'bottom-end' },
)
} catch (error: any) {
toast.push(
<Notification type="danger">
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
const { cultureName, roleId, userId } = filterRef.current
await getFields(cultureName, roleId, userId)
setIsSavingOrder(false)
}
}
if (!fields) {
return <></>
}
const orderedFields = sortFields(fields, orderMode)
return (
<>
<Formik
@ -333,6 +397,7 @@ function FormFields({
}}
onSubmit={async ({ cultureName, roleId, userId }, { setSubmitting }) => {
setSubmitting(true)
filterRef.current = { cultureName, roleId, userId }
getFields(cultureName, roleId, userId).then(() => {
toast.push(
<Notification type="success" duration={100}>
@ -384,120 +449,119 @@ function FormFields({
<AdaptableCard className="h-full" bodyClass="h-full">
<div className="grid 2xl:grid-cols-5 xl:grid-cols-3 lg:grid-cols-5 gap-4 h-full border-t-2">
<div className="2xl:col-span-2 xl:col-span-1 lg:col-span-2 border-r-2">
<Table compact>
<THead>
<Tr>
<Th className="w-3/12">
<div>
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaFileMedical />}
onClick={() => {
setIsNewField(true)
setRow({} as ColumnFormatEditDto)
}}
/>
)}
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Copy"
icon={<FaCopy />}
onClick={async (e) => {
e.preventDefault()
setIsCopyField(true)
}}
/>
)}
{canCreate && allDbColumns.length > 0 && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title={translate('::App.ListFormField.AddAllColumns')}
icon={<FaTable />}
loading={isAddingAllColumns}
onClick={addAllColumns}
/>
)}
</div>
</Th>
<Th className="w-3/12">Field</Th>
<Th className="w-2/12">Culture</Th>
<Th className="hidden sm:table-cell w-2/12">Role</Th>
<Th className="hidden sm:table-cell w-2/12">User</Th>
</Tr>
</THead>
<TBody>
{fields.map((field) => (
<Tr
key={field.id}
className={classNames(
'cursor-pointer',
field.id === row?.id ? 'bg-sozsoft-active dark:bg-gray-600' : '',
)}
onClick={() => {
setRow(field)
}}
>
<Td className="flex items-center !py-1 !px-1">
{canDelete && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Delete"
icon={<FaMinus />}
onClick={async (e) => {
e.preventDefault()
if (field.id) {
setDeleteRowId(field.id)
}
}}
/>
)}
<span style={{ visibility: !field?.visible ? 'visible' : 'hidden' }}>
<FaEyeSlash />
</span>
<span style={{ visibility: !field?.isActive ? 'visible' : 'hidden' }}>
<FaTimes />
</span>
<span className="ml-5">
{field.listOrderNo}
{field.editOrderNo ? ` (${field.editOrderNo})` : ''}
</span>
</Td>
<Td className="!py-1 !px-1">{field.fieldName}</Td>
<Td className="align-middle text-center !py-1 !px-1">
<Avatar
size={18}
shape="circle"
src={`/img/countries/${field.cultureName ?? 'default'}.png`}
title={field.cultureName}
<div className="flex justify-end px-2 py-2">
<Segment
size="xs"
value={[orderMode]}
onChange={(value) => {
// Tekli Segment secili ogeye tekrar tiklaninca bos deger gonderir.
const next = Array.isArray(value) ? value[0] : value
if (next === 'list' || next === 'edit') {
setOrderMode(next)
}
}}
>
<Segment.Item value="list">
{translate('::App.ListFormFieldEdit.OptionsListOrderNo')}
</Segment.Item>
<Segment.Item value="edit">
{translate('::App.ListFormFieldEdit.EditFormOrderNo')}
</Segment.Item>
</Segment>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={orderedFields.map((f) => f.id!)}
strategy={verticalListSortingStrategy}
>
<Table compact>
<THead>
<Tr>
<Th className="w-3/12">
<div>
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaFileMedical />}
onClick={() => {
setIsNewField(true)
setRow({} as ColumnFormatEditDto)
}}
/>
)}
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Copy"
icon={<FaCopy />}
onClick={async (e) => {
e.preventDefault()
setIsCopyField(true)
}}
/>
)}
{canCreate && allDbColumns.length > 0 && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title={translate('::App.ListFormField.AddAllColumns')}
icon={<FaTable />}
loading={isAddingAllColumns}
onClick={addAllColumns}
/>
)}
</div>
</Th>
<Th className="w-3/12">Field</Th>
<Th className="w-2/12">Culture</Th>
<Th className="hidden sm:table-cell w-2/12">Role</Th>
<Th className="hidden sm:table-cell w-2/12">User</Th>
</Tr>
</THead>
<TBody>
{orderedFields.map((field) => (
<SortableFieldRow
key={field.id}
field={field}
isSelected={field.id === row?.id}
canDelete={canDelete}
dragDisabled={
!canUpdate ||
isSavingOrder ||
(orderMode === 'edit' && field.editOrderNo == null)
}
dragTitle={translate('::App.ListFormField.DragToReorder')}
roleLabel={
field.roleId
? (roleList.find((a) => a.value === field.roleId)?.label ?? 'X')
: undefined
}
userLabel={
field.userId
? (userList.find((a) => a.value === field.userId)?.label ?? 'X')
: undefined
}
onSelect={() => setRow(field)}
onDelete={() => field.id && setDeleteRowId(field.id)}
/>
</Td>
<Td className="hidden sm:table-cell !py-1 !px-1">
{field.roleId &&
(roleList.find((a) => a.value === field.roleId)?.label ?? 'X')}
</Td>
<Td className="hidden sm:table-cell !py-1 !px-1">
{field.userId &&
(userList.find((a) => a.value === field.userId)?.label ?? 'X')}
</Td>
</Tr>
))}
</TBody>
</Table>
))}
</TBody>
</Table>
</SortableContext>
</DndContext>
</div>
<div className="2xl:col-span-3 lg:col-span-3 xl:col-span-2">
<FormFieldEdit

View file

@ -0,0 +1,100 @@
import { Avatar, Button } from '@/components/ui'
import Td from '@/components/ui/Table/Td'
import Tr from '@/components/ui/Table/Tr'
import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import classNames from 'classnames'
import { FaEyeSlash, FaGripVertical, FaMinus, FaTimes } from 'react-icons/fa'
function SortableFieldRow({
field,
isSelected,
canDelete,
dragDisabled,
dragTitle,
roleLabel,
userLabel,
onSelect,
onDelete,
}: {
field: ColumnFormatEditDto
isSelected: boolean
canDelete: boolean
dragDisabled: boolean
dragTitle: string
roleLabel?: string
userLabel?: string
onSelect: () => void
onDelete: () => void
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: field.id!,
disabled: dragDisabled,
})
return (
<Tr
ref={setNodeRef}
style={{ transform: CSS.Translate.toString(transform), transition }}
className={classNames(
'cursor-pointer',
isSelected ? 'bg-sozsoft-active dark:bg-gray-600' : '',
isDragging ? 'relative z-10 opacity-60' : '',
)}
onClick={onSelect}
>
<Td className="flex items-center !py-1 !px-1">
<Button
type="button"
variant="plain"
shape="circle"
size="xs"
title={dragTitle}
disabled={dragDisabled}
icon={<FaGripVertical />}
className={dragDisabled ? 'invisible' : 'cursor-grab'}
{...attributes}
{...listeners}
/>
{canDelete && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Delete"
icon={<FaMinus />}
onClick={(e) => {
e.preventDefault()
onDelete()
}}
/>
)}
<span style={{ visibility: !field?.visible ? 'visible' : 'hidden' }}>
<FaEyeSlash />
</span>
<span style={{ visibility: !field?.isActive ? 'visible' : 'hidden' }}>
<FaTimes />
</span>
<span className="ml-5">
{field.listOrderNo}
{field.editOrderNo ? ` (${field.editOrderNo})` : ''}
</span>
</Td>
<Td className="!py-1 !px-1">{field.fieldName}</Td>
<Td className="align-middle text-center !py-1 !px-1">
<Avatar
size={18}
shape="circle"
src={`/img/countries/${field.cultureName ?? 'default'}.png`}
title={field.cultureName}
/>
</Td>
<Td className="hidden sm:table-cell !py-1 !px-1">{roleLabel}</Td>
<Td className="hidden sm:table-cell !py-1 !px-1">{userLabel}</Td>
</Tr>
)
}
export default SortableFieldRow

View file

@ -0,0 +1,73 @@
import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
export type FieldOrderMode = 'list' | 'edit'
/**
* Sürükleme sonucunu sıra numaralarına çevirir. Liste sırası bütün alanlar üzerindedir; form sırası
* yalnızca aynı form grubundaki, forma giren (editOrderNo dolu) alanlar arasındadır — gruplar arası
* taşıma grup değişikliğidir ve Options sekmesinden yapılır. Numaralar kapsam içinde 1'den yeniden
* dağıtılır, yalnızca değeri değişen alanlar döner.
*/
export function reorderFields(
fields: ColumnFormatEditDto[],
mode: FieldOrderMode,
activeId: string,
overId: string,
): ColumnFormatEditDto[] {
const active = fields.find((f) => f.id === activeId)
const over = fields.find((f) => f.id === overId)
if (!active || !over || active === over) {
return []
}
// Filtresiz listede ayni alanin baska kultur/rol/kullanici kayitlari da durur; her biri kendi
// sirasini tasir.
const sameOwner = (f: ColumnFormatEditDto) =>
f.cultureName === active.cultureName &&
(f.roleId ?? null) === (active.roleId ?? null) &&
(f.userId ?? null) === (active.userId ?? null)
const scope = !sameOwner(over)
? []
: mode === 'list'
? sortFields(fields.filter(sameOwner), 'list')
: active.editOrderNo != null &&
over.editOrderNo != null &&
active.editGroupOrderNo === over.editGroupOrderNo
? sortFields(
fields.filter(
(f) =>
sameOwner(f) &&
f.editOrderNo != null &&
f.editGroupOrderNo === active.editGroupOrderNo,
),
'edit',
)
: []
const from = scope.indexOf(active)
const to = scope.indexOf(over)
if (from < 0 || to < 0) {
return []
}
const moved = [...scope]
moved.splice(to, 0, ...moved.splice(from, 1))
const key = mode === 'list' ? 'listOrderNo' : 'editOrderNo'
return moved
.map((f, index) => ({ field: f, order: index + 1 }))
.filter(({ field, order }) => field[key] !== order)
.map(({ field, order }) => ({ ...field, [key]: order }))
}
export function sortFields(fields: ColumnFormatEditDto[], mode: FieldOrderMode) {
const last = Number.MAX_SAFE_INTEGER
return [...fields].sort((a, b) =>
mode === 'list'
? (a.listOrderNo ?? last) - (b.listOrderNo ?? last)
: (a.editGroupOrderNo ?? last) - (b.editGroupOrderNo ?? last) ||
(a.editOrderNo ?? last) - (b.editOrderNo ?? last) ||
(a.listOrderNo ?? last) - (b.listOrderNo ?? last),
)
}